迁移892-without-redis分支全量功能

This commit is contained in:
wkc
2026-04-15 14:18:56 +08:00
parent 9fe1bffe0d
commit 79c5317414
97 changed files with 10922 additions and 232 deletions

View File

@@ -4,8 +4,9 @@ VUE_APP_TITLE = 若依管理系统
# 开发环境配置
ENV = 'development'
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
# 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api'
VUE_APP_PASSWORD_TRANSFER_KEY = '1234567890abcdef'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@@ -4,5 +4,6 @@ VUE_APP_TITLE = 若依管理系统
# 生产环境配置
ENV = 'production'
# 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api'
# 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api'
VUE_APP_PASSWORD_TRANSFER_KEY = '1234567890abcdef'

View File

@@ -28,6 +28,7 @@
"axios": "0.30.3",
"clipboard": "2.0.8",
"core-js": "3.37.1",
"crypto-js": "4.2.0",
"echarts": "5.4.0",
"element-ui": "2.15.14",
"file-saver": "2.0.5",
@@ -54,6 +55,7 @@
"chalk": "4.1.0",
"compression-webpack-plugin": "6.1.2",
"connect": "3.6.6",
"html-webpack-plugin": "3.2.0",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5",

View File

@@ -0,0 +1,45 @@
import request from '@/utils/request'
// 查询利率定价流程列表
export function listWorkflow(query) {
return request({
url: '/loanPricing/workflow/list',
method: 'get',
params: query
})
}
// 查询利率定价流程详情
export function getWorkflow(serialNum) {
return request({
url: '/loanPricing/workflow/' + serialNum,
method: 'get'
})
}
// 创建个人客户利率定价流程
export function createPersonalWorkflow(data) {
return request({
url: '/loanPricing/workflow/create/personal',
method: 'post',
data: data
})
}
// 创建企业客户利率定价流程
export function createCorporateWorkflow(data) {
return request({
url: '/loanPricing/workflow/create/corporate',
method: 'post',
data: data
})
}
// 设定执行利率
export function setExecuteRate(serialNum, executeRate) {
return request({
url: '/loanPricing/workflow/' + serialNum + '/executeRate',
method: 'put',
data: { executeRate: executeRate }
})
}

View File

@@ -1,14 +1,15 @@
import request from '@/utils/request'
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
import request from '@/utils/request'
import { encryptPasswordFields } from '@/utils/passwordTransfer'
// 登录方法
export function login(username, password, code, uuid) {
const data = encryptPasswordFields({
username,
password,
code,
uuid
}, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
return request({
url: '/login',
headers: {
isToken: false,
@@ -17,19 +18,20 @@ export function login(username, password, code, uuid) {
method: 'post',
data: data
})
}
// 注册方法
export function register(data) {
return request({
url: '/register',
}
// 注册方法
export function register(data) {
const payload = encryptPasswordFields(data, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
return request({
url: '/register',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
},
method: 'post',
data: payload
})
}
// 获取用户详细信息
export function getInfo() {
@@ -66,4 +68,4 @@ export function getCodeImg() {
method: 'get',
timeout: 20000
})
}
}

View File

@@ -1,5 +1,6 @@
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi"
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi"
import { encryptPasswordFields } from '@/utils/passwordTransfer'
// 查询用户列表
export function listUser(query) {
@@ -19,13 +20,14 @@ export function getUser(userId) {
}
// 新增用户
export function addUser(data) {
return request({
url: '/system/user',
method: 'post',
data: data
})
}
export function addUser(data) {
const payload = encryptPasswordFields(data, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
return request({
url: '/system/user',
method: 'post',
data: payload
})
}
// 修改用户
export function updateUser(data) {
@@ -45,12 +47,12 @@ export function delUser(userId) {
}
// 用户密码重置
export function resetUserPwd(userId, password) {
const data = {
userId,
password
}
return request({
export function resetUserPwd(userId, password) {
const data = encryptPasswordFields({
userId,
password
}, ['password'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
return request({
url: '/system/user/resetPwd',
method: 'put',
data: data
@@ -88,12 +90,12 @@ export function updateUserProfile(data) {
}
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword
}
return request({
export function updateUserPwd(oldPassword, newPassword) {
const data = encryptPasswordFields({
oldPassword,
newPassword
}, ['oldPassword', 'newPassword'], process.env.VUE_APP_PASSWORD_TRANSFER_KEY)
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
data: data

View File

@@ -0,0 +1,14 @@
import CryptoJS from 'crypto-js'
export function encryptPasswordFields(payload, fields, key) {
const next = { ...payload }
fields.forEach((field) => {
if (next[field]) {
next[field] = CryptoJS.AES.encrypt(next[field], CryptoJS.enc.Utf8.parse(key), {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString()
}
})
return next
}

View File

@@ -0,0 +1,100 @@
<template>
<el-card class="bargaining-pool-card">
<div slot="header" class="card-header">
<span class="card-title">议价池</span>
</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="网点议价池">
{{ displayBranchPool }}
</el-descriptions-item>
<el-descriptions-item label="支行议价池">
{{ displaySubBranchPool }}
</el-descriptions-item>
<el-descriptions-item label="私域池">
{{ displayPrivateDomainPool }}
</el-descriptions-item>
<el-descriptions-item label="超利分成">
{{ displayExcessProfitShare }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</template>
<script>
export default {
name: "BargainingPoolDisplay",
props: {
branchPool: {
type: [Number, String],
default: 0
},
subBranchPool: {
type: [Number, String],
default: 0
},
privateDomainPool: {
type: [Number, String],
default: 0
},
excessProfitShare: {
type: [Number, String],
default: 0
}
},
computed: {
displayBranchPool() {
const value = this.branchPool
if (value === null || value === undefined || value === '') {
return '0'
}
return value
},
displaySubBranchPool() {
const value = this.subBranchPool
if (value === null || value === undefined || value === '') {
return '0'
}
return value
},
displayPrivateDomainPool() {
const value = this.privateDomainPool
if (value === null || value === undefined || value === '') {
return '0'
}
return value
},
displayExcessProfitShare() {
const value = this.excessProfitShare
if (value === null || value === undefined || value === '') {
return '0'
}
return value
}
}
}
</script>
<style lang="scss" scoped>
.bargaining-pool-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;
}
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<el-dialog title="新增企业利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body
@close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="客户内码" prop="custIsn">
<el-input v-model="form.custIsn" placeholder="请输入客户内码"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="custName">
<el-input v-model="form.custName" placeholder="请输入客户名称"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="证件类型" prop="idType">
<el-select v-model="form.idType" placeholder="请选择证件类型" style="width: 100%">
<el-option label="统一社会信用代码" value="统一社会信用代码"/>
<el-option label="其他" value="其他"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="证件号码" prop="idNum">
<el-input v-model="form.idNum" placeholder="请输入证件号码"/>
</el-form-item>
</el-col>
</el-row>
<!-- 贷款信息 -->
<el-divider content-position="left">贷款信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="担保方式" prop="guarType">
<el-select v-model="form.guarType" placeholder="请选择担保方式" style="width: 100%">
<el-option label="信用" value="信用"/>
<el-option label="保证" value="保证"/>
<el-option label="抵押" value="抵押"/>
<el-option label="质押" value="质押"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请金额(元)" prop="applyAmt">
<el-input v-model="form.applyAmt" placeholder="请输入申请金额"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="贷款期限(月)" prop="loanTerm">
<el-input v-model.number="form.loanTerm" type="number" placeholder="请输入贷款期限"/>
</el-form-item>
</el-col>
</el-row>
<!-- 企业标识 -->
<el-divider content-position="left">企业标识</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="省农担担保贷款" prop="isAgriGuar">
<el-switch v-model="form.isAgriGuar"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="绿色贷款" prop="isGreenLoan">
<el-switch v-model="form.isGreenLoan"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="科技型企业" prop="isTechEnt">
<el-switch v-model="form.isTechEnt"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="贸易和建筑业企业" prop="isTradeConstruction">
<el-switch v-model="form.isTradeConstruction"/>
</el-form-item>
</el-col>
</el-row>
<!-- 抵质押信息 -->
<el-divider content-position="left">抵质押信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="抵质押类型" prop="collType">
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
<el-option label="一线" value="一线"/>
<el-option label="一类" value="一类"/>
<el-option label="二类" value="二类"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
<el-switch v-model="form.collThirdParty"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitting" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import {createCorporateWorkflow} from "@/api/loanPricing/workflow"
export default {
name: "CorporateCreateDialog",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
// 金额验证
const validateApplyAmt = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入申请金额'))
} else {
const num = parseFloat(value)
if (isNaN(num) || num <= 0) {
callback(new Error('请输入有效的金额'))
} else if (num > 999999999.99) {
callback(new Error('金额不能超过 999999999.99'))
} else {
callback()
}
}
}
// 贷款期限验证
const validateLoanTerm = (rule, value, callback) => {
if (!value && value !== 0) {
callback(new Error('请输入贷款期限'))
} else {
const num = parseInt(value)
if (isNaN(num) || num <= 0) {
callback(new Error('请输入有效的贷款期限'))
} else if (num > 360) {
callback(new Error('贷款期限不能超过 360 个月'))
} else {
callback()
}
}
}
return {
submitting: false,
form: {
orgCode: '892000',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanTerm: undefined,
isAgriGuar: false,
isGreenLoan: false,
isTechEnt: false,
isTradeConstruction: false,
collType: undefined,
collThirdParty: false
},
rules: {
custIsn: [
{required: true, message: "客户内码不能为空", trigger: "blur"},
{min: 1, max: 50, message: "长度在 1 到 50 个字符", trigger: "blur"}
],
custName: [
{required: true, message: "客户名称不能为空", trigger: "blur"},
{min: 1, max: 100, message: "长度在 1 到 100 个字符", trigger: "blur"}
],
idType: [
{required: true, message: "请选择证件类型", trigger: "change"}
],
idNum: [
{required: true, message: "证件号码不能为空", trigger: "blur"}
],
guarType: [
{required: true, message: "请选择担保方式", trigger: "change"}
],
applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"}
],
loanTerm: [
{required: true, validator: validateLoanTerm, trigger: "blur"}
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
watch: {
visible(val) {
if (val) {
this.reset()
}
}
},
methods: {
/** 表单重置 */
reset() {
this.form = {
orgCode: '892000',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanTerm: undefined,
isAgriGuar: false,
isGreenLoan: false,
isTechEnt: false,
isTradeConstruction: false,
collType: undefined,
collThirdParty: false
}
this.submitting = false
this.resetForm("form")
},
/** 对话框关闭处理 */
handleClose() {
this.cancel()
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false
this.reset()
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.submitting = true
// 转换开关值为字符串
const data = {
...this.form,
isAgriGuar: this.form.isAgriGuar ? 'true' : 'false',
isGreenLoan: this.form.isGreenLoan ? 'true' : 'false',
isTechEnt: this.form.isTechEnt ? 'true' : 'false',
isTradeConstruction: this.form.isTradeConstruction ? 'true' : 'false',
collThirdParty: this.form.collThirdParty ? 'true' : 'false'
}
createCorporateWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
this.dialogVisible = false
this.$emit('success')
}).catch(error => {
// 保留表单,显示错误信息
console.error('创建失败:', error)
}).finally(() => {
this.submitting = false
})
}
})
}
}
}
</script>
<style scoped>
.workflow-create-form .el-divider {
margin: 8px 0 16px 0;
font-weight: 500;
font-size: 14px;
}
@media screen and (max-width: 768px) {
.workflow-create-form .el-col {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
}
</style>

View File

@@ -0,0 +1,324 @@
<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">
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="null"
:corp-output="corpOutput"
/>
<!-- 流程详情卡片 -->
<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>
<!-- 议价池卡片 -->
<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 || value === '1' || value === 1) return '是'
if (value === 'false' || value === false || value === '0' || value === 0) 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>

View File

@@ -0,0 +1,105 @@
<template>
<el-dialog title="选择客户类型" :visible.sync="dialogVisible" width="500px" append-to-body
:close-on-click-modal="false">
<div class="customer-type-selector">
<div class="type-card" @click="selectType('personal')">
<div class="card-icon">
<i class="el-icon-user"></i>
</div>
<div class="card-content">
<div class="card-title">个人客户</div>
<div class="card-desc">个人客户利率定价</div>
</div>
</div>
<div class="type-card" @click="selectType('corporate')">
<div class="card-icon">
<i class="el-icon-office-building"></i>
</div>
<div class="card-content">
<div class="card-title">企业客户</div>
<div class="card-desc">企业客户利率定价</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: "CustomerTypeSelector",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
selectType(type) {
this.$emit('select', type)
this.dialogVisible = false
}
}
}
</script>
<style scoped>
.customer-type-selector {
display: flex;
gap: 20px;
padding: 20px 0;
}
.type-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.type-card:hover {
border-color: #409EFF;
background-color: #f0f7ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.card-icon {
font-size: 48px;
color: #409EFF;
margin-bottom: 15px;
}
.card-content {
text-align: center;
}
.card-title {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 14px;
color: #909399;
}
</style>

View File

@@ -0,0 +1,311 @@
<template>
<el-card class="model-output-card" v-if="custType && (retailOutput || corpOutput)">
<div slot="header" class="card-header">
<span class="card-title">模型输出</span>
</div>
<el-tabs v-model="activeTab">
<!-- 个人客户模型输出 -->
<template v-if="custType === '个人' && retailOutput">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="retail-basic">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="客户内码">{{ retailOutput.custIsn || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ retailOutput.custName || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ retailOutput.idType || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ retailOutput.idNum || '-' }}</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ retailOutput.baseLoanRate || '-' }}</span> %</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 忠诚度分析 -->
<el-tab-pane label="忠诚度分析" name="retail-loyalty">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="我行首贷客户">{{ formatBoolean(retailOutput.isFirstLoan) }}</el-descriptions-item>
<el-descriptions-item label="用信天数">{{ retailOutput.faithDay || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户年龄">{{ retailOutput.custAge || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_首贷"><span class="bp-value">{{ retailOutput.bpFirstLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="BP_贷龄"><span class="bp-value">{{ retailOutput.bpAgeLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="BP_年龄"><span class="bp-value">{{ retailOutput.bpAge || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_忠诚度" :span="2"><span class="total-bp-value">{{ retailOutput.totalBpLoyalty || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 贡献度分析 -->
<el-tab-pane label="贡献度分析" name="retail-contribution">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="存款年日均">{{ retailOutput.balanceAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款年日均">{{ retailOutput.loanAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ retailOutput.derivationRate || '-' }}</el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_贡献度"><span class="total-bp-value">{{ retailOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 关联度分析 -->
<el-tab-pane label="关联度分析" name="retail-relevance">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="中间业务_个人_信用卡">{{ formatBoolean(retailOutput.midPerCard) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_一码通">{{ formatBoolean(retailOutput.midPerPass) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_丰收互联">{{ formatBoolean(retailOutput.midPerHarvest) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_有效客户">{{ formatBoolean(retailOutput.midPerEffect) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_快捷支付">{{ formatBoolean(retailOutput.midPerQuickPay) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_电费代扣">{{ formatBoolean(retailOutput.midPerEleDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_水费代扣">{{ formatBoolean(retailOutput.midPerWaterDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_华数费代扣">{{ formatBoolean(retailOutput.midPerHuashuDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_煤气费代扣">{{ formatBoolean(retailOutput.MidPerGasDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_市民卡">{{ formatBoolean(retailOutput.midPerCitizencard) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_理财业务">{{ formatBoolean(retailOutput.midPerFinMan) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_etc">{{ formatBoolean(retailOutput.midPerEtc) }}</el-descriptions-item>
<el-descriptions-item label="BP_中间业务"><span class="bp-value">{{ retailOutput.bpMid || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ retailOutput.totoalBpRelevance || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 贷款特征 -->
<el-tab-pane label="贷款特征" name="retail-loan">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="申请金额">{{ retailOutput.applyAmt || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款额度"><span class="bp-value">{{ retailOutput.bpLoanAmount || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="贷款用途">{{ formatLoanPurpose(retailOutput.loanPurpose) }}</el-descriptions-item>
<el-descriptions-item label="是否有经营佐证">{{ formatBoolean(retailOutput.bizProof) }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款用途"><span class="bp-value">{{ retailOutput.bpLoanUse || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="循环功能">{{ formatBoolean(retailOutput.loanLoop) }}</el-descriptions-item>
<el-descriptions-item label="BP_循环功能"><span class="bp-value">{{ retailOutput.bpLoanLoop || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ retailOutput.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(retailOutput.collThirdParty) }}</el-descriptions-item>
<el-descriptions-item label="BP_抵押物"><span class="bp-value">{{ retailOutput.bpCollateral || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 风险度分析 -->
<el-tab-pane label="风险度分析" name="retail-risk">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="灰名单客户">{{ formatBoolean(retailOutput.greyCust) }}</el-descriptions-item>
<el-descriptions-item label="本金逾期">{{ formatBoolean(retailOutput.prinOverdue) }}</el-descriptions-item>
<el-descriptions-item label="利息逾期">{{ formatBoolean(retailOutput.interestOverdue) }}</el-descriptions-item>
<el-descriptions-item label="信用卡逾期">{{ formatBoolean(retailOutput.cardOverdue) }}</el-descriptions-item>
<el-descriptions-item label="BP_灰名单与逾期"><span class="bp-value">{{ retailOutput.bpGreyOverdue || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ retailOutput.totoalBpRisk || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 测算结果 -->
<el-tab-pane label="测算结果" name="retail-result">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="浮动BP"><span class="total-bp-value">{{ retailOutput.totalBp || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ retailOutput.calculateRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ retailOutput.loanRateHistory || '-' }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ retailOutput.minRateProduct || '-' }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ retailOutput.smoothRange || '-' }}</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ retailOutput.finalCalculateRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ retailOutput.referenceRate || '-' }}</span> %</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</template>
<!-- 企业客户模型输出 -->
<template v-else-if="custType === '企业' && corpOutput">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="corp-basic">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="客户内码">{{ corpOutput.custIsn || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ corpOutput.custName || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ corpOutput.idType || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ corpOutput.idNum || '-' }}</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ corpOutput.baseLoanRate || '-' }}</span> %</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 忠诚度分析 -->
<el-tab-pane label="忠诚度分析" name="corp-loyalty">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="我行首贷客户">{{ formatBoolean(corpOutput.isFirstLoan) }}</el-descriptions-item>
<el-descriptions-item label="用信天数">{{ corpOutput.faithDay || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_首贷"><span class="bp-value">{{ corpOutput.bpFirstLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="BP_贷龄"><span class="bp-value">{{ corpOutput.bpAgeLoan || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_忠诚度" :span="2"><span class="total-bp-value">{{ corpOutput.totalBpLoyalty || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 贡献度分析 -->
<el-tab-pane label="贡献度分析" name="corp-contribution">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="存款年日均">{{ corpOutput.balanceAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款年日均">{{ corpOutput.loanAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ corpOutput.derivationRate || '-' }}</el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_贡献度"><span class="total-bp-value">{{ corpOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 关联度分析 -->
<el-tab-pane label="关联度分析" name="corp-relevance">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="中间业务_企业_企业互联">{{ formatBoolean(corpOutput.midEntConnect) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_有效价值客户">{{ formatBoolean(corpOutput.midEntEffect) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_国际业务">{{ formatBoolean(corpOutput.midEntInter) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_承兑">{{ formatBoolean(corpOutput.midEntAccept) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_贴现">{{ formatBoolean(corpOutput.midEntDiscount) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_电费代扣">{{ formatBoolean(corpOutput.midEntEleDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_水费代扣">{{ formatBoolean(corpOutput.midEntWaterDdc) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_企业_税务代扣">{{ formatBoolean(corpOutput.midEntTax) }}</el-descriptions-item>
<el-descriptions-item label="BP_中间业务"><span class="bp-value">{{ corpOutput.bpMid || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="代发工资户数">{{ corpOutput.payroll || '-' }}</el-descriptions-item>
<el-descriptions-item label="存量贷款余额">{{ corpOutput.invLoanAmount || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_代发工资"><span class="bp-value">{{ corpOutput.bpPayroll || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ corpOutput.totoalBpRelevance || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 企业类别 -->
<el-tab-pane label="企业类别" name="corp-category">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="净身企业">{{ formatBoolean(corpOutput.isCleanEnt) }}</el-descriptions-item>
<el-descriptions-item label="开立基本结算账户">{{ formatBoolean(corpOutput.hasSettleAcct) }}</el-descriptions-item>
<el-descriptions-item label="省农担担保贷款">{{ formatBoolean(corpOutput.isAgriGuar) }}</el-descriptions-item>
<el-descriptions-item label="绿色贷款">{{ formatBoolean(corpOutput.isGreenLoan) }}</el-descriptions-item>
<el-descriptions-item label="科技型企业">{{ formatBoolean(corpOutput.isTechEnt) }}</el-descriptions-item>
<el-descriptions-item label="BP_企业客户类别"><span class="bp-value">{{ corpOutput.bpEntType || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 贷款特征 -->
<el-tab-pane label="贷款特征" name="corp-loan">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="贷款期限">{{ corpOutput.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款期限"><span class="bp-value">{{ corpOutput.bpLoanTerm || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="申请金额">{{ corpOutput.applyAmt || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_贷款额度"><span class="bp-value">{{ corpOutput.bpLoanAmount || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ corpOutput.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(corpOutput.collThirdParty) }}</el-descriptions-item>
<el-descriptions-item label="BP_抵押物"><span class="bp-value">{{ corpOutput.bpCollateral || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 风险度分析 -->
<el-tab-pane label="风险度分析" name="corp-risk">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="灰名单客户">{{ formatBoolean(corpOutput.greyCust) }}</el-descriptions-item>
<el-descriptions-item label="本金逾期">{{ formatBoolean(corpOutput.prinOverdue) }}</el-descriptions-item>
<el-descriptions-item label="利息逾期">{{ formatBoolean(corpOutput.interestOverdue) }}</el-descriptions-item>
<el-descriptions-item label="信用卡逾期">{{ formatBoolean(corpOutput.cardOverdue) }}</el-descriptions-item>
<el-descriptions-item label="BP_灰名单与逾期"><span class="bp-value">{{ corpOutput.bpGreyOverdue || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ corpOutput.totoalBpRisk || '-' }}</span></el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 测算结果 -->
<el-tab-pane label="测算结果" name="corp-result">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="浮动BP"><span class="total-bp-value">{{ corpOutput.totalBp || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ corpOutput.calculateRate || '-' }}</span> %</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</template>
</el-tabs>
</el-card>
</template>
<script>
export default {
name: "ModelOutputDisplay",
props: {
custType: {
type: String,
default: null
},
retailOutput: {
type: Object,
default: null
},
corpOutput: {
type: Object,
default: null
}
},
data() {
return {
activeTab: ''
}
},
watch: {
custType: {
immediate: true,
handler(val) {
// 根据客户类型设置默认 tab
if (val === '个人') {
this.activeTab = 'retail-basic'
} else if (val === '企业') {
this.activeTab = 'corp-basic'
}
}
}
},
methods: {
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true || value === '1' || value === 1) return '是'
if (value === 'false' || value === false || value === '0' || value === 0) return '否'
return value || '-'
},
/** 格式化贷款用途 */
formatLoanPurpose(value) {
if (value === 'consumer') return '消费贷款'
if (value === 'business') return '经营贷款'
return value || '-'
}
}
}
</script>
<style lang="scss" scoped>
.model-output-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;
}
::v-deep .el-tabs__header {
margin-bottom: 20px;
}
// BP 值样式
.bp-value {
color: #409eff;
font-weight: 500;
}
// TOTAL_BP 样式
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
// 测算利率高亮样式
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
// 利率值样式
.rate-value {
color: #67c23a;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,281 @@
<template>
<el-dialog title="新增个人利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body
@close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="客户内码" prop="custIsn">
<el-input v-model="form.custIsn" placeholder="请输入客户内码"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="custName">
<el-input v-model="form.custName" placeholder="请输入客户名称"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="证件类型" prop="idType">
<el-select v-model="form.idType" placeholder="请选择证件类型" style="width: 100%">
<el-option label="身份证" value="身份证"/>
<el-option label="其他证件" value="其他证件"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="证件号码" prop="idNum">
<el-input v-model="form.idNum" placeholder="请输入证件号码"/>
</el-form-item>
</el-col>
</el-row>
<!-- 贷款信息 -->
<el-divider content-position="left">贷款信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="担保方式" prop="guarType">
<el-select v-model="form.guarType" placeholder="请选择担保方式" style="width: 100%">
<el-option label="信用" value="信用"/>
<el-option label="保证" value="保证"/>
<el-option label="抵押" value="抵押"/>
<el-option label="质押" value="质押"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请金额(元)" prop="applyAmt">
<el-input v-model="form.applyAmt" placeholder="请输入申请金额"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="贷款用途" prop="loanPurpose">
<el-select v-model="form.loanPurpose" placeholder="请选择贷款用途" style="width: 100%">
<el-option label="消费" value="consumer"/>
<el-option label="经营" value="business"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="借款期限(年)" prop="loanTerm">
<el-select v-model="form.loanTerm" placeholder="请选择借款期限" style="width: 100%">
<el-option v-for="item in loanTermOptions" :key="item" :label="item" :value="item"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="是否有经营佐证" prop="bizProof">
<el-switch v-model="form.bizProof"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="循环功能" prop="loanLoop">
<el-switch v-model="form.loanLoop"/>
</el-form-item>
</el-col>
</el-row>
<!-- 抵质押信息 -->
<el-divider content-position="left">抵质押信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="抵质押类型" prop="collType">
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
<el-option label="一类" value="一类"/>
<el-option label="二类" value="二类"/>
<el-option label="三类" value="三类"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
<el-switch v-model="form.collThirdParty"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitting" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import {createPersonalWorkflow} from "@/api/loanPricing/workflow"
export default {
name: "PersonalCreateDialog",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
// 金额验证
const validateApplyAmt = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入申请金额'))
} else {
const num = parseFloat(value)
if (isNaN(num) || num <= 0) {
callback(new Error('请输入有效的金额'))
} else if (num > 999999999.99) {
callback(new Error('金额不能超过 999999999.99'))
} else {
callback()
}
}
}
return {
loanTermOptions: [
'1', '2', '3', '4', '5', '6'
],
submitting: false,
form: {
orgCode: '892000',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
},
rules: {
custIsn: [
{required: true, message: "客户内码不能为空", trigger: "blur"},
{min: 1, max: 50, message: "长度在 1 到 50 个字符", trigger: "blur"}
],
custName: [
{required: true, message: "客户名称不能为空", trigger: "blur"},
{min: 1, max: 100, message: "长度在 1 到 100 个字符", trigger: "blur"}
],
idType: [
{required: true, message: "请选择证件类型", trigger: "change"}
],
idNum: [
{required: true, message: "证件号码不能为空", trigger: "blur"}
],
guarType: [
{required: true, message: "请选择担保方式", trigger: "change"}
],
applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"}
],
loanPurpose: [
{required: true, message: "请选择贷款用途", trigger: "change"}
],
loanTerm: [
{required: true, message: "请选择借款期限", trigger: "change"}
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
watch: {
visible(val) {
if (val) {
this.reset()
}
}
},
methods: {
/** 表单重置 */
reset() {
this.form = {
orgCode: '892000',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
}
this.submitting = false
this.resetForm("form")
},
/** 对话框关闭处理 */
handleClose() {
this.cancel()
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false
this.reset()
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.submitting = true
// 转换开关值为字符串
const data = {
...this.form,
bizProof: this.form.bizProof ? '1' : '0',
loanLoop: this.form.loanLoop ? '1' : '0',
collThirdParty: this.form.collThirdParty ? '1' : '0'
}
createPersonalWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
this.dialogVisible = false
this.$emit('success')
}).catch(error => {
// 保留表单,显示错误信息
console.error('创建失败:', error)
}).finally(() => {
this.submitting = false
})
}
})
}
}
}
</script>
<style scoped>
.workflow-create-form .el-divider {
margin: 8px 0 16px 0;
font-weight: 500;
font-size: 14px;
}
@media screen and (max-width: 768px) {
.workflow-create-form .el-col {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
}
</style>

View File

@@ -0,0 +1,330 @@
<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">
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="retailOutput"
:corp-output="null"
/>
<!-- 流程详情卡片 -->
<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="贷款用途">{{ formatLoanPurpose(detailData.loanPurpose) }}</el-descriptions-item>
<el-descriptions-item label="借款期限">{{ detailData.loanTerm || '-' }}</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>
<!-- 议价池卡片 -->
<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 || value === '1' || value === 1) return '是'
if (value === 'false' || value === false || value === '0' || value === 0) return '否'
return value || '-'
},
/** 格式化贷款用途 */
formatLoanPurpose(value) {
if (value === 'consumer') return '消费'
if (value === 'business') return '经营'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.retailOutput?.baseLoanRate || '-'
},
/** 获取浮动BP */
getTotalBp() {
return this.retailOutput?.totalBp || '-'
},
/** 获取最终测算利率 */
getCalculateRate() {
return this.retailOutput?.finalCalculateRate || '-'
},
/** 设定执行利率 */
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>

View File

@@ -0,0 +1,97 @@
<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>

View File

@@ -0,0 +1,182 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="客户内码" prop="custIsn">
<el-input
v-model="queryParams.custIsn"
placeholder="请输入客户内码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="创建者" prop="createBy">
<el-input
v-model="queryParams.createBy"
placeholder="请输入创建者"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="机构号" prop="orgCode">
<el-input
v-model="queryParams.orgCode"
placeholder="请输入机构号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="workflowList">
<el-table-column label="业务方流水号" align="center" prop="serialNum" width="180" :show-overflow-tooltip="true" />
<el-table-column label="客户名称" align="center" prop="custName" :show-overflow-tooltip="true" />
<el-table-column label="客户类型" align="center" prop="custType" width="100" />
<el-table-column label="担保方式" align="center" prop="guarType" width="100" />
<el-table-column label="申请金额(元)" align="center" prop="applyAmt" width="120" />
<el-table-column label="测算利率(%)" align="center" prop="calculateRate" width="100" />
<el-table-column label="执行利率(%)" align="center" prop="executeRate" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="创建者" align="center" prop="createBy" width="120" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
>查看</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 客户类型选择对话框 -->
<customer-type-selector :visible.sync="showTypeSelector" @select="handleSelectType"/>
<!-- 个人客户创建对话框 -->
<personal-create-dialog :visible.sync="showPersonalDialog" @success="handleCreateSuccess"/>
<!-- 企业客户创建对话框 -->
<corporate-create-dialog :visible.sync="showCorporateDialog" @success="handleCreateSuccess"/>
</div>
</template>
<script>
import {listWorkflow} from "@/api/loanPricing/workflow"
import CustomerTypeSelector from "./components/CustomerTypeSelector"
import PersonalCreateDialog from "./components/PersonalCreateDialog"
import CorporateCreateDialog from "./components/CorporateCreateDialog"
export default {
name: "LoanPricingWorkflow",
components: {
CustomerTypeSelector,
PersonalCreateDialog,
CorporateCreateDialog
},
data() {
return {
// 遮罩层
loading: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 利率定价流程表格数据
workflowList: [],
// 是否显示客户类型选择弹出层
showTypeSelector: false,
// 是否显示个人客户创建弹出层
showPersonalDialog: false,
// 是否显示企业客户创建弹出层
showCorporateDialog: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
custIsn: undefined,
createBy: undefined,
orgCode: undefined
}
}
},
created() {
this.getList()
},
activated() {
this.getList()
},
methods: {
/** 查询利率定价流程列表 */
getList() {
this.loading = true
listWorkflow(this.queryParams).then(response => {
this.workflowList = response.rows
this.total = response.total
this.loading = false
})
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
/** 查看详情操作 */
handleView(row) {
this.$router.push({
name: 'LoanPricingWorkflowDetail',
params: { serialNum: row.serialNum }
})
},
/** 新增按钮操作 */
handleAdd() {
this.showTypeSelector = true
},
/** 选择客户类型回调 */
handleSelectType(type) {
if (type === 'personal') {
this.showPersonalDialog = true
} else if (type === 'corporate') {
this.showCorporateDialog = true
}
},
/** 创建成功回调 */
handleCreateSuccess() {
this.getList()
}
}
}
</script>

View File

@@ -73,13 +73,13 @@ export default {
return {
title: process.env.VUE_APP_TITLE,
footerContent: defaultSettings.footerContent,
codeUrl: "",
loginForm: {
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
codeUrl: "",
loginForm: {
username: "",
password: "",
rememberMe: false,
code: "",
uuid: ""
},
loginRules: {
username: [

View File

@@ -0,0 +1,34 @@
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
const corporateCreateDialog = read('src/views/loanPricing/workflow/components/CorporateCreateDialog.vue')
assert(
!personalCreateDialog.includes('const validateIdNum ='),
'个人新增弹窗仍包含证件号码格式校验函数'
)
assert(
!corporateCreateDialog.includes('const validateIdNum ='),
'企业新增弹窗仍包含证件号码格式校验函数'
)
assert(
personalCreateDialog.includes("idNum: [") &&
personalCreateDialog.includes('{required: true, message: "证件号码不能为空", trigger: "blur"}'),
'个人新增弹窗证件号码规则应仅保留必填'
)
assert(
corporateCreateDialog.includes("idNum: [") &&
corporateCreateDialog.includes('{required: true, message: "证件号码不能为空", trigger: "blur"}'),
'企业新增弹窗证件号码规则应仅保留必填'
)
console.log('id number validation removal assertions passed')

View File

@@ -0,0 +1,30 @@
const fs = require('fs')
const path = require('path')
const assert = require('assert')
const loginViewSource = fs.readFileSync(
path.join(__dirname, '../src/views/login.vue'),
'utf8'
)
assert(
/loginForm:\s*\{[\s\S]*username:\s*""/.test(loginViewSource),
'登录页默认用户名应为空字符串'
)
assert(
/loginForm:\s*\{[\s\S]*password:\s*""/.test(loginViewSource),
'登录页默认密码应为空字符串'
)
assert(
/username:\s*username === undefined \? this\.loginForm\.username : username/.test(loginViewSource),
'登录页应继续支持从 cookie 回填用户名'
)
assert(
/password:\s*password === undefined \? this\.loginForm\.password : decrypt\(password\)/.test(loginViewSource),
'登录页应继续支持从 cookie 回填密码'
)
console.log('login default credentials assertions passed')

View File

@@ -0,0 +1,88 @@
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function loadModule(filePath, stubs = {}) {
const source = fs.readFileSync(filePath, 'utf8')
const exportedNames = []
const transformed = source
.replace(/^import .*$/gm, '')
.replace(/export function\s+([A-Za-z0-9_]+)\s*\(/g, (_, name) => {
exportedNames.push(name)
return `function ${name}(`
})
.replace(/export default\s+/g, 'module.exports = ')
const sandbox = {
module: { exports: {} },
exports: {},
require,
console,
process: {
env: {
VUE_APP_PASSWORD_TRANSFER_KEY: '1234567890abcdef'
}
},
...stubs
}
vm.runInNewContext(
`${transformed}\nmodule.exports = { ${exportedNames.join(', ')} };`,
sandbox,
{ filename: filePath }
)
return sandbox.module.exports
}
const passwordTransferModule = loadModule(
path.resolve(__dirname, '../src/utils/passwordTransfer.js'),
{ CryptoJS: require('crypto-js') }
)
const { encryptPasswordFields } = passwordTransferModule
const encrypted = encryptPasswordFields(
{ password: 'admin123', code: '8888' },
['password'],
'1234567890abcdef'
)
assert.notStrictEqual(encrypted.password, 'admin123')
assert.strictEqual(encrypted.code, '8888')
const request = config => config
const loginModule = loadModule(
path.resolve(__dirname, '../src/api/login.js'),
{ request, encryptPasswordFields }
)
const loginConfig = loginModule.login('admin', 'admin123', '8888', 'uuid-1')
assert.notStrictEqual(loginConfig.data.password, 'admin123')
assert.strictEqual(loginConfig.data.username, 'admin')
const registerConfig = loginModule.register({ username: 'u1', password: 'p1', confirmPassword: 'p1', code: '8888' })
assert.notStrictEqual(registerConfig.data.password, 'p1')
assert.strictEqual(registerConfig.data.confirmPassword, 'p1')
const userModule = loadModule(
path.resolve(__dirname, '../src/api/system/user.js'),
{
request,
encryptPasswordFields,
parseStrEmpty: value => value
}
)
const updatePwdConfig = userModule.updateUserPwd('oldPwd', 'newPwd')
assert.notStrictEqual(updatePwdConfig.data.oldPassword, 'oldPwd')
assert.notStrictEqual(updatePwdConfig.data.newPassword, 'newPwd')
const addUserConfig = userModule.addUser({ userName: 'u1', password: 'initPwd', nickName: 'n1' })
assert.notStrictEqual(addUserConfig.data.password, 'initPwd')
const resetUserPwdConfig = userModule.resetUserPwd(2, 'resetPwd')
assert.notStrictEqual(resetUserPwdConfig.data.password, 'resetPwd')
console.log('password-transfer-api test passed')

View File

@@ -0,0 +1,65 @@
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
assert(
personalCreateDialog.includes('label="贷款用途"') && personalCreateDialog.includes('prop="loanPurpose"'),
'个人新增弹窗缺少贷款用途字段'
)
assert(
personalCreateDialog.includes('label="借款期限(年)"') && personalCreateDialog.includes('prop="loanTerm"'),
'个人新增弹窗缺少借款期限字段'
)
assert(
personalCreateDialog.includes("value=\"consumer\"") && personalCreateDialog.includes("value=\"business\""),
'个人新增弹窗缺少贷款用途选项'
)
assert(
personalCreateDialog.includes('loanTermOptions') &&
personalCreateDialog.includes("'1'") &&
personalCreateDialog.includes("'6'") &&
!personalCreateDialog.includes("'7'"),
'个人新增弹窗借款期限选项应限制为 1-6 年'
)
assert(
personalCreateDialog.includes('label="一类"') &&
personalCreateDialog.includes('label="二类"') &&
personalCreateDialog.includes('label="三类"') &&
!personalCreateDialog.includes('label="一线"'),
'个人新增弹窗抵质押类型选项未按 Excel 对齐'
)
assert(
!personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'),
'个人新增弹窗仍将抵质押类型设为必填'
)
assert(
personalCreateDialog.includes("bizProof: this.form.bizProof ? '1' : '0'") &&
personalCreateDialog.includes("loanLoop: this.form.loanLoop ? '1' : '0'") &&
personalCreateDialog.includes("collThirdParty: this.form.collThirdParty ? '1' : '0'"),
'个人新增弹窗开关字段未按 1/0 提交'
)
assert(
personalDetail.includes('label="贷款用途"') && personalDetail.includes('detailData.loanPurpose'),
'个人详情页缺少贷款用途展示'
)
assert(
personalDetail.includes("value === '1'") && personalDetail.includes("value === '0'"),
'个人详情页布尔格式化未兼容 1/0'
)
console.log('personal create input params assertions passed')

View File

@@ -0,0 +1,21 @@
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
assert(
/label="最终测算利率"/.test(personalDetail),
'个人流程详情左侧缺少“最终测算利率”标签'
)
assert(
/return this\.retailOutput\?\.finalCalculateRate \|\| '-'/.test(personalDetail),
'个人流程详情没有使用 finalCalculateRate 展示最终测算利率'
)
console.log('personal final calculate rate display assertions passed')

View File

@@ -0,0 +1,29 @@
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const modelOutput = read('src/views/loanPricing/workflow/components/ModelOutputDisplay.vue')
assert(
personalDetail.includes('label="借款期限"') && personalDetail.includes('detailData.loanTerm'),
'个人详情页缺少借款期限展示'
)
const requiredRetailFields = [
'retailOutput.loanRateHistory',
'retailOutput.minRateProduct',
'retailOutput.smoothRange',
'retailOutput.finalCalculateRate',
'retailOutput.referenceRate'
]
requiredRetailFields.forEach((field) => {
assert(modelOutput.includes(field), `模型输出缺少字段展示: ${field}`)
})
console.log('retail display fields assertions passed')

View File

@@ -0,0 +1,27 @@
const fs = require('fs')
const path = require('path')
const assert = require('assert')
function read(relativePath) {
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}
function assertModelOutputBeforeDetailCard(source, label) {
const modelOutputIndex = source.indexOf('<ModelOutputDisplay')
const detailCardIndex = source.indexOf('<el-card class="detail-card">')
assert(modelOutputIndex !== -1, `${label} 缺少模型输出卡片`)
assert(detailCardIndex !== -1, `${label} 缺少流程详情卡片`)
assert(
modelOutputIndex < detailCardIndex,
`${label} 的模型输出卡片应位于流程详情卡片上方`
)
}
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const corporateDetail = read('src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue')
assertModelOutputBeforeDetailCard(personalDetail, '个人流程详情')
assertModelOutputBeforeDetailCard(corporateDetail, '企业流程详情')
console.log('workflow detail card order assertions passed')

View File

@@ -0,0 +1,52 @@
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const vm = require('vm')
function loadComponentOptions(filePath) {
const source = fs.readFileSync(filePath, 'utf8')
const scriptMatch = source.match(/<script>([\s\S]*?)<\/script>/)
if (!scriptMatch) {
throw new Error('未找到组件脚本内容')
}
const importNames = []
const importPattern = /^import\s+([A-Za-z0-9_]+)\s+from\s+.*$/gm
let importMatch = importPattern.exec(scriptMatch[1])
while (importMatch) {
importNames.push(importMatch[1])
importMatch = importPattern.exec(scriptMatch[1])
}
const stubImports = importNames.map(name => `const ${name} = {};`).join('\n')
const transformed = `${stubImports}\n${scriptMatch[1]}`
.replace(/^import .*$/gm, '')
.replace(/export default/, 'module.exports =')
const sandbox = {
module: { exports: {} },
exports: {},
require,
console
}
vm.runInNewContext(transformed, sandbox, { filename: filePath })
return sandbox.module.exports
}
const filePath = path.resolve(__dirname, '../src/views/loanPricing/workflow/index.vue')
const component = loadComponentOptions(filePath)
assert.strictEqual(typeof component.activated, 'function', '流程列表页应在激活时刷新数据')
let refreshCount = 0
component.activated.call({
getList() {
refreshCount += 1
}
})
assert.strictEqual(refreshCount, 1, '流程列表页激活时应调用一次 getList')
console.log('workflow-index-refresh test passed')