Files
intc-vue3/src/views/invest/onlineLendAnalysis/index.vue
2026-03-06 22:28:45 +08:00

816 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<div class="search-con">
<div class="title">查询条件</div>
<el-form :model="queryParams" ref="queryRef" :inline="true" :rules="rules" label-width="100px">
<el-form-item label="贷款机构" prop="bankCardLendId">
<el-select v-model="queryParams.id" placeholder="请选择贷款机构" @change="handleCreditChange" clearable>
<el-option v-for="creditCard in creditCardList" :key="creditCard.id" :label="creditCard.nameCode" :value="creditCard.id" />
</el-select>
</el-form-item>
<el-form-item label="" prop="type">
<el-select v-model="queryParams.type" @change="handleTimeChange" placeholder="请选择日期">
<el-option v-for="item in dates" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="" prop="time" v-if="queryParams.type === 1">
<el-date-picker
v-model="queryParams.time"
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD"
@calendar-change="calendarChange"
/>
</el-form-item>
<el-form-item label="" prop="time" v-if="queryParams.type === 2">
<el-date-picker
v-model="queryParams.time"
type="monthrange"
range-separator="~"
format="YYYY-MM"
start-placeholder="开始时间"
end-placeholder="结束时间"
@calendar-change="calendarChange"
/>
</el-form-item>
<el-form-item label="" prop="time" v-if="queryParams.type === 3">
<yearPicker
v-model="queryParams.time"
ref="statisticPicker"
labelText="选择年份"
:initYear="dateValue"
:showYear="showYearValue"
:maxLength="10"
sp="~"
@updateTimeRange="updateStatisticYear"
/>
</el-form-item>
</el-form>
<div class="search-btn-con">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="info" icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</div>
<div class="main-con">
<div class="summary-con">
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#409EFF"><List /></el-icon>
</div>
<div class="item-wrap">
<div class="title">未结清贷款笔数</div>
<div class="num">{{ historyObj.unclearedDetailCount }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#67C23A"><WalletFilled /></el-icon>
</div>
<div class="item-wrap">
<div class="title">未结清本息</div>
<div class="num">{{ historyObj.unClearedDetail }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#E6A23C"><Money /></el-icon>
</div>
<div class="item-wrap">
<div class="title">未结清本金</div>
<div class="num">{{ historyObj.unClearedPrincipal }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#F56C6C"><Coin /></el-icon>
</div>
<div class="item-wrap">
<div class="title">未结清利息</div>
<div class="num">{{ historyObj.unClearedInterest }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#909399"><DocumentCopy /></el-icon>
</div>
<div class="item-wrap">
<div class="title">当期应还本息</div>
<div class="num">{{ historyObj.dueDetail }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#67C23A"><CircleCheck /></el-icon>
</div>
<div class="item-wrap">
<div class="title">当期已还本息</div>
<div class="num">{{ historyObj.repaidDetail }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#E6A23C"><Clock /></el-icon>
</div>
<div class="item-wrap">
<div class="title">当期待还本息</div>
<div class="num">{{ historyObj.leftDetail }}</div>
</div>
</div>
<div class="center-con">
<div class="icon-box">
<el-icon :size="40" color="#F56C6C"><Histogram /></el-icon>
</div>
<div class="item-wrap">
<div class="title">当期利息</div>
<div class="num">{{ historyObj.dueInterest }}</div>
</div>
</div>
</div>
<div class="title-con">
<div class="title">贷款还款计划</div>
<div class="operate-btn-con">
<el-radio-group v-model="radioVal" @change="handleRadioChange">
<el-radio-button label="柱状图" />
<el-radio-button label="折线图" />
<el-radio-button label="表格" />
</el-radio-group>
</div>
</div>
<div class="content-con">
<div v-show="radioVal === '柱状图'" class="chart" id="chartBar"></div>
<div v-show="radioVal === '折线图'" class="chart" id="chartLine"></div>
<el-table v-show="radioVal === '表格'" v-loading="loading" :data="reversedTableData">
<el-table-column label="序号" width="50" type="index" align="center">
<template #default="scope">
<span>{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="还款日期" align="center" prop="time" width="200" />
<el-table-column label="待还款金额" align="center" prop="value" />
<el-table-column prop="detail" label="待还款明细" width="900" v-if="queryParams.type === 1">
<template #default="scope">
<div v-if="scope.row.detailList && scope.row.detailList.length > 0">
<div
v-for="(group, groupIndex) in groupByRepaymentAccount(scope.row.detailList)"
:key="groupIndex"
style="padding: 6px 8px; margin-bottom: 4px; background: #f5f7fa; border-radius: 4px"
>
<!-- 该账户下的所有明细 -->
<div
v-for="(item, itemIndex) in group.items"
:key="itemIndex"
:style="{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '4px 0',
borderBottom: itemIndex < group.items.length - 1 ? '1px dashed #dcdfe6' : 'none'
}"
>
<span style="flex: 1"
>{{ item.bankCardLendName }}(本金:{{ item.principal }},利息:{{ item.interest }},合计:{{ item.currentAmount }})</span
>
<el-button size="small" type="primary" icon="Coin" @click="handleRepayment(item)">还款</el-button>
</div>
<!-- 还款账户信息同一账户只显示一次放在明细后面 -->
<div
v-if="group.repaymentAccountName"
style="margin-top: 4px; padding-top: 4px; color: #409eff; font-weight: 500; border-top: 1px solid #e4e7ed"
>
还款账户:{{ group.repaymentAccountName }}(余额:{{ group.repaymentAccountBalance }})
</div>
</div>
</div>
<span v-else v-html="formatMultiLineData(scope.row.detail)"></span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 还款对话框 -->
<el-dialog title="贷款还款" v-model="repaymentOpen" width="500px" append-to-body>
<el-form ref="repaymentFormRef" :model="repaymentForm" :rules="repaymentRules" label-width="120px">
<el-form-item label="贷款机构">
<el-input v-model="repaymentForm.bankCardLendName" disabled />
</el-form-item>
<el-form-item label="还款日期" prop="repaymentDate">
<el-date-picker
clearable
v-model="repaymentForm.repaymentDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择还款日期"
style="width: 100%"
>
</el-date-picker>
</el-form-item>
<el-form-item label="应还本金" prop="principal">
<el-input v-model="repaymentForm.principal" type="number" @input="calculateTotalAmount" placeholder="请输入应还本金" />
</el-form-item>
<el-form-item label="应还利息" prop="interest">
<el-input v-model="repaymentForm.interest" type="number" @input="calculateTotalAmount" placeholder="请输入应还利息" />
</el-form-item>
<el-form-item label="合计金额">
<el-input v-model="repaymentForm.currentAmount" disabled />
</el-form-item>
<el-form-item label="还款状态" prop="postingState">
<el-select v-model="repaymentForm.postingState" placeholder="请选择还款状态" style="width: 100%">
<el-option label="已还款" value="1"></el-option>
<el-option label="未还款" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="repaymentForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitRepayment"> </el-button>
<el-button @click="repaymentOpen = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="CreditAnalysis">
import dayjs from 'dayjs'
import * as echarts from 'echarts'
import { ElMessage } from 'element-plus'
import { List, WalletFilled, Money, Coin, DocumentCopy, CircleCheck, Clock, TrendCharts, Histogram } from '@element-plus/icons-vue'
import yearPicker from '@/components/YearPicker/index.vue'
import { getInstallmentHistoryAnalysis } from '@/api/invest/statisticAnalysis'
import { listBankcardLend } from '@/api/invest/bankcardlend'
const { proxy } = getCurrentInstance()
const dateValue = ref({ startYear: 2000, endYear: new Date().getFullYear() + 5 })
const showYearValue = ref({ startShowYear: '', endShowYear: '' })
const updateStatisticYear = (startYear, endYear) => {
queryParams.value.time = [new Date(startYear, 0, 1), new Date(endYear, 0, 1)]
if (endYear - startYear > 10) {
ElMessage.warning('时间跨度不能大于10年默认选择开始日期的10年')
queryParams.value.time = [new Date(startYear, 0, 1), new Date(startYear + 10, 0, 1)]
}
}
const dates = [
{ value: 1, label: '日' },
{ value: 2, label: '月' },
{ value: 3, label: '年' }
]
const loading = ref(true)
const total = ref(0)
const radioVal = ref('柱状图')
const repaymentOpen = ref(false)
const creditCardList = ref([])
const dateValidate = (rules, value, callback) => {
const dateType = rules.dateType || 'days'
const num = rules.num || 31
const dateString = rules.dateString || '天'
const message = rules.message || `时间跨度不能超过${num}${dateString}`
if (value && value.length === 2) {
const start = value[0]
const end = value[1]
if (dayjs(end).diff(dayjs(start), dateType) > num) {
queryParams.value.time = [start, dayjs(start).add(35, 'months')]
ElMessage.warning(message)
}
}
}
const data = reactive({
queryParams: {
type: 1,
time: null,
dataType: '3',
id: null,
state: null
},
queryCreditCardParams: {
pageNum: 1,
type: '3',
status: '1',
lendType: '1',
pageSize: 1000
},
rules: {
time: [{ validator: dateValidate, dateType: 'months', num: 35, dateRange: '月', message: '时间跨度不能大于36个月默认选择开始日期的36个月' }]
},
repaymentForm: {
id: null,
bankCardLendName: '',
repaymentDate: '',
principal: 0,
interest: 0,
currentAmount: 0,
postingState: '1',
remark: ''
},
repaymentRules: {
repaymentDate: [{ required: true, message: '还款日期不能为空', trigger: 'blur' }],
principal: [{ required: true, message: '应还本金不能为空', trigger: 'blur' }],
interest: [{ required: true, message: '应还利息不能为空', trigger: 'blur' }],
postingState: [{ required: true, message: '还款状态不能为空', trigger: 'change' }]
}
})
const { queryParams, queryCreditCardParams, rules, repaymentForm, repaymentRules } = toRefs(data)
const handleCreditChange = (id) => {
queryParams.value.id = id
getList()
}
const today = new Date()
const start = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-01'
// 当前月份的结束日期
const endDate = new Date(today.getFullYear(), today.getMonth() + 2, 0)
const end = `${endDate.getFullYear()}-${(endDate.getMonth() + 1).toString().padStart(2, '0')}-${endDate.getDate().toString().padStart(2, '0')}`
queryParams.value.time = [start, end]
/** 查询信用卡卡管理列表 */
function getCreditCardList() {
listBankcardLend(queryCreditCardParams.value).then((response) => {
creditCardList.value = response.rows
})
}
function formatMultiLineData(data) {
if (data != null) {
return data.replace(/<br\/>/g, '<br/>')
}
}
const handleTimeChange = (type) => {
queryParams.value.time = null
if (type === 1) {
const today = new Date()
const start = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-01'
// 当前月份的结束日期
const endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0)
const end = `${endDate.getFullYear()}-${(endDate.getMonth() + 1).toString().padStart(2, '0')}-${endDate.getDate().toString().padStart(2, '0')}`
queryParams.value.time = [start, end]
} else if (type === 2) {
const today = new Date()
const start = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2)
const end = dayjs(start).add(35, 'months')
queryParams.value.time = [start, end]
} else if (type === 3) {
const today = new Date()
const startYear = today.getFullYear()
queryParams.value.time = [new Date(startYear, 0, 1), new Date(startYear + 10, 0, 1)]
showYearValue.value = { startShowYear: startYear, endShowYear: startYear + 10 }
}
getList()
}
const firstChooseDate = ref('')
const calendarChange = (val) => {
firstChooseDate.value = val[0].getTime()
if (val[1]) firstChooseDate.value = ''
}
const historyObj = ref({
unclearedDetailCount: '',
unClearedDetail: '',
dueDetail: '',
repaidDetail: '',
leftDetail: '',
historyDetailList: [],
tableHistoryDetailList: []
})
const chartData = ref({
name: [],
value1: []
})
// 计算属性:倒序后的表格数据
const reversedTableData = computed(() => {
if (historyObj.value.tableHistoryDetailList && historyObj.value.tableHistoryDetailList.length > 0) {
return [...historyObj.value.tableHistoryDetailList].reverse()
}
return []
})
// 按还款账户分组处理明细数据
const groupByRepaymentAccount = (detailList) => {
if (!detailList || detailList.length === 0) return []
const groups = {}
detailList.forEach((item) => {
const key = item.repaymentAccountName || '未指定账户'
if (!groups[key]) {
groups[key] = {
repaymentAccountName: item.repaymentAccountName,
repaymentAccountBalance: item.repaymentAccountBalance,
items: []
}
}
groups[key].items.push(item)
})
return Object.values(groups)
}
function getList() {
loading.value = true
chartData.value = { name: [], value1: [] }
const { type, time, dataType, id } = queryParams.value
let formatValue = 'YYYY-MM-DD'
if (type === 1) {
formatValue = 'YYYY-MM-DD'
} else if (type === 2) {
formatValue = 'YYYY-MM'
} else {
formatValue = 'YYYY'
}
const params = {
type,
startTime: time && time.length > 0 ? dayjs(time[0]).format(formatValue) : '',
endTime: time && time.length > 0 ? dayjs(time[1]).format(formatValue) : '',
id,
dataType
}
getInstallmentHistoryAnalysis(params).then((response) => {
loading.value = false
historyObj.value = { ...response.data }
historyObj.value.historyDetailList.map((item) => {
return {
name: item.time,
value: item.value
}
})
response.data.historyDetailList.map((item) => {
chartData.value.name.push(item.time)
chartData.value.value1.push(item.value)
})
handleRadioChange(currentType.value)
})
}
function handleQuery() {
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
queryParams.value.id = null
queryParams.value.dataType = '3'
total.value = 0
handleQuery()
}
const drawBar = (data) => {
if (document.getElementById('chartBar') === null) {
return
}
echarts.dispose(document.getElementById('chartBar'))
const myChart = echarts.init(document.getElementById('chartBar'))
const option = {
legend: {
// 图示例样式
show: true,
top: 50,
right: 50,
itemGap: 20
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
return params[0].name + '<br>' + '贷款待还金额:' + params[0].value + '元'
}
},
grid: {
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: data.name,
axisTick: {
alignWithLabel: true // 刻度线是否与标签对齐默认false
},
// 设置 X 轴线条粗细
axisLine: {
lineStyle: {
width: 1, // 根据需求调整数值大小,单位是像素
color: '#999'
}
}
}
],
yAxis: [
{
type: 'value',
axisTick: {
show: false // 默认为true如果要隐藏则改为 false
},
axisLine: {
show: true,
lineStyle: {
width: 1,
color: '#999'
}
}
}
],
series: [
{
data: data.value1,
type: 'bar',
barWidth: 20, // 根据需求调整数值大小,单位是像素
itemStyle: {
color: '#2283cf'
}
}
],
// 添加点击事件的处理函数
emphasis: {
itemStyle: {
color: '#2283cf',
barWidth: 20 // 根据需求调整数值大小,单位是像素
},
barWidth: 24 // 根据需求调整数值大小,单位是像素
}
}
myChart.setOption(option)
}
const drawLine = (data) => {
if (document.getElementById('chartLine') === null) {
return
}
echarts.dispose(document.getElementById('chartLine'))
const myChart = echarts.init(document.getElementById('chartLine'))
const option = {
grid: {
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
return params[0].name + '<br>' + '贷款待还金额' + params[0].value + '元'
}
},
xAxis: {
type: 'category',
data: data.name,
axisTick: {
alignWithLabel: true
},
axisLine: {
lineStyle: {
width: 1,
color: '#999'
}
}
},
yAxis: {
type: 'value',
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
width: 1,
color: '#999'
}
}
},
series: [
{
data: data.value1,
type: 'line',
lineStyle: {
color: '#4181c9'
},
itemStyle: {
color: '#4181c9'
},
smooth: true,
symbol: 'emptyCircle',
areaStyle: {
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: 'rgba(65,129,201, 0.70)'
},
{
offset: 1,
color: 'rgba(65,129,201, 0.10)'
}
],
false
),
shadowColor: 'rgba(65,129,201, 0.10)',
shadowBlur: 10
}
}
]
}
myChart.setOption(option)
}
const currentType = ref('柱状图')
const handleRadioChange = (type) => {
currentType.value = type
switch (type) {
case '柱状图':
drawBar(chartData.value)
break
case '折线图':
drawLine(chartData.value)
break
default:
break
}
}
/** 还款按钮操作 */
function handleRepayment(item) {
repaymentForm.value = {
id: item.id,
bankCardLendName: item.bankCardLendName,
repaymentDate: dayjs(new Date().getTime()).format('YYYY-MM-DD'),
principal: item.principal,
interest: item.interest,
currentAmount: item.currentAmount,
postingState: '1',
remark: ''
}
repaymentOpen.value = true
}
/** 计算合计金额 */
function calculateTotalAmount() {
const principal = parseFloat(repaymentForm.value.principal) || 0
const interest = parseFloat(repaymentForm.value.interest) || 0
repaymentForm.value.currentAmount = (principal + interest).toFixed(2)
}
/** 提交还款 */
function submitRepayment() {
proxy.$refs.repaymentFormRef.validate((valid) => {
if (valid) {
proxy.$modal
.confirm('是否确认已还款?')
.then(function () {
// TODO: 这里需要调用后端接口更新还款状态
// 示例: return updateInstallmentDetail(repaymentForm.value)
proxy.$modal.msgSuccess('还款成功')
repaymentOpen.value = false
getList() // 刷新列表
})
.catch(() => {})
}
})
}
getCreditCardList()
getList()
</script>
<style lang="scss" scoped>
.main-con {
height: calc(100vh - 200px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.summary-con {
gap: 0.14rem !important;
justify-content: flex-start !important;
flex-wrap: wrap;
margin-bottom: 0.08rem !important;
flex-shrink: 0;
.center-con {
flex: 1 1 calc(25% - 0.105rem);
min-width: 2.6rem;
max-width: calc(25% - 0.105rem);
list-style: none !important;
&::before,
&::after {
display: none !important;
content: none !important;
}
}
.item-wrap {
list-style: none !important;
&::before,
&::after {
display: none !important;
content: none !important;
}
.num,
.title {
&::before,
&::after {
display: none !important;
content: none !important;
}
}
}
.icon-box {
&::before,
&::after {
display: none !important;
content: none !important;
}
}
}
.title-con {
margin-top: 0 !important;
padding-top: 0.08rem !important;
flex-shrink: 0;
}
.content-con {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
.chart {
flex: 1;
min-height: 0;
width: 100%;
}
.el-table {
flex: 1;
}
}
// 响应式优化
@media (max-width: 1600px) {
.summary-con {
.center-con {
flex: 1 1 calc(33.333% - 0.093rem);
max-width: calc(33.333% - 0.093rem);
}
}
}
@media (max-width: 1200px) {
.summary-con {
.center-con {
flex: 1 1 calc(50% - 0.07rem);
max-width: calc(50% - 0.07rem);
min-width: 2.4rem;
}
}
}
@media (max-width: 768px) {
.summary-con {
.center-con {
flex: 1 1 100%;
max-width: 100%;
}
}
}
</style>