同步6/16

This commit is contained in:
Zy
2026-06-01 18:12:58 +08:00
parent 38274ab612
commit 50e7b14fd6
183 changed files with 8956 additions and 4803 deletions

View File

@@ -3,86 +3,103 @@
<div class="flex flex-items-center searchBox">
<div class="searchLabel">选择日期</div>
<el-date-picker
v-model="date"
v-model="dataTime"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 400px; margin-right: 10px"
></el-date-picker>
<el-button type="primary">查询</el-button>
<el-button>重置</el-button>
<el-button type="primary" @click="querySearch">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div style="margin-bottom: 15px; position: relative; z-index: 2">
<el-row :gutter="20">
<el-col :lg="14" :xl="14">
<div class="cardbox">
<div>收入来源</div>
<div class="card_infoall">
<div>应缴费用{{ formatMoneyWithUnit(form.totalDue) }}</div>
<div>已收{{ formatMoneyWithUnit(form.totalPaid) }}</div>
<div>待收{{ formatMoneyWithUnit(form.totalUnpaid) }}</div>
</div>
<div class="cardbody" style="height: 165px">
<div
v-for="(opt, index) in incomeChartOptions"
:key="index"
class="chart-item flex flex-col items-center"
style="width: 165px; height: 100%"
>
<Echarts :options="opt.options as EChartsOption" style="width: 100%; height: 165px"></Echarts>
<!-- 如果需要额外显示名称可以在这里加 div但图表中心已显示 -->
<div class="chart-item-name">{{ opt.name }}</div>
</div>
</div>
</div>
</el-col>
<el-row :gutter="20">
<el-col :lg="14" :xl="14">
<div class="cardbox">
<div>收入来源</div>
<div class="cardbody" style="height: 200px">
<div class="flex flex-col flex-items-center flex-1">
<Echarts :options="options"></Echarts>
<div>停车费</div>
</div>
<div class="flex flex-col flex-items-center flex-1">
<Echarts :options="options"></Echarts>
<div>物业费</div>
</div>
<div class="flex flex-col flex-items-center flex-1">
<Echarts :options="options"></Echarts>
<div>商铺租金</div>
</div>
<div class="flex flex-col flex-items-center flex-1">
<Echarts :options="options"></Echarts>
<div>水电费</div>
</div>
<div class="flex flex-col flex-items-center flex-1">
<Echarts :options="options"></Echarts>
<div>维修费</div>
<el-col :lg="10" :xl="10">
<div class="cardbox">
<div>欠费分布</div>
<div class="cardbody w-full" style="height: 150px">
<Echarts :options="options2"></Echarts>
</div>
</div>
</div>
</el-col>
<el-col :lg="10" :xl="8">
<div class="cardbox">
<div>缴费方式</div>
<div class="cardbody w-full" style="height: 150px">
<Echarts :options="options2"></Echarts>
</el-col>
</el-row>
</div>
<div style="margin-bottom: 15px; position: relative; z-index: 2">
<el-row :gutter="20">
<el-col :lg="12" :xl="14">
<div class="cardbox w-full">
<div>欠费分布</div>
<div class="cardbody w-full" style="height: 330px">
<Echarts :options="options3"></Echarts>
</div>
</div>
</div>
</el-col>
</el-row>
<div class="" style="height: 20px"></div>
<el-row :gutter="20">
<el-col :lg="14" :xl="14">
<div class="cardbox w-full" style="height: 400px">
<Echarts :options="options3"></Echarts>
</div>
</el-col>
<el-col :lg="10" :xl="8">
<div class="cardbox w-full" style="height: 400px">
<div class="" style="height: 50px; margin-bottom: 20px">缴费方式</div>
<div flex flex-items-center justify-center>
<ul class="legendbox">
<li>小程序</li>
<li>物业缴费</li>
<li>手机APP</li>
</ul>
<ul class="circlebox">
<li>70%</li>
<li>20%</li>
<li>10%</li>
</ul>
</el-col>
<el-col :lg="12" :xl="10">
<div class="cardbox w-full" style="height: 400px">
<div style="height: 50px; margin-bottom: 20px">缴费来源</div>
<div flex flex-items-center justify-center>
<ul class="legendbox">
<li v-for="(item, index) in paytypelist" :key="index">
<i
:style="{
backgroundColor: item.color
}"
></i>
<span class="ml-2">{{ item.label }}</span>
</li>
</ul>
<ul class="circlebox">
<li :data-level="index" :style="{ backgroundColor: item.color }" v-for="(item, index) in paytypelist" :key="index">
{{ item.number }}%
</li>
</ul>
</div>
</div>
</div>
</el-col>
</el-row>
<div class="" style="height: 20px"></div>
<el-row>
<el-col :span="24">
<div class="cardbox w-full" style="height: 450px">
<Echarts :options="flowLineOption"></Echarts>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
<div style="position: relative; z-index: 2">
<el-row>
<el-col :span="24">
<!-- <div class="cardbox w-full" style="height: 450px">
<Echarts :options="flowLineOption"></Echarts>
</div> -->
<div class="cardbox w-full">
<div>缴费走势分析</div>
<div class="cardbody w-full" style="height: 400px">
<Echarts :options="flowLineOption"></Echarts>
</div>
</div>
</el-col>
</el-row>
</div>
<Teleport to="body">
<i v-if="open" class="bgColor"></i>
</Teleport>
</div>
</template>
@@ -90,88 +107,52 @@
// TODO 缴费分析
import { ref } from 'vue';
import Echarts from '@/components/Echarts/index.vue';
import { formatMoneyWithUnit } from '@/utils/money';
import { EChartsOption } from 'echarts';
import { getLast7Days, getLastNDays } from '@/utils/time';
import { getLast7Days, getPastDayDate } from '@/utils/time';
import { income_statsApi } from '@/api/system/Payment/index';
const date = ref('');
/** 入住情况 */
const options = shallowRef<EChartsOption>({
tooltip: { show: false },
series: [
// 1. 底层细圆环(灰色底色)
{
type: 'pie',
radius: ['60%', '80%'], // 细一点
clockwise: true,
data: [100],
itemStyle: { color: '#ebeef5' },
emphasis: { disabled: true },
label: { show: false }
},
// 2. 上层粗圆环(进度条)
{
type: 'pie',
radius: ['60%', '85%'], // 粗一点
clockwise: true,
itemStyle: {
color: '#409eff',
borderRadius: 10
},
emphasis: { disabled: true },
label: {
show: true,
position: 'center',
fontSize: 20,
fontWeight: 'bold',
formatter: '85%'
},
data: [{ value: 85 }, { value: 15, itemStyle: { color: 'transparent' } }]
}
]
const open = ref(false);
const dataTime = ref('2025-01-01');
const form = ref({
'totalDue': '',
'totalPaid': '',
'totalUnpaid': '',
'paidPercentage': '',
'payWx': null,
'payZfb': null,
'payOffline': null,
'totalPay': null,
'feeTypeStats': [],
'dailyStats': []
});
/** 入住类型 */
const options1 = shallowRef<EChartsOption>({
legend: {
top: 'center',
left: '0',
orient: 'vertical',
itemGap: 50
const defaultDay = 30; // 默认查询近30天数据
/** 收入来源 */
/** 收入来源图表配置 */
const incomeChartOptions = ref<
{
name: string;
options?: EChartsOption;
}[]
>([
{
'name': '停车费'
},
series: [
{
name: 'Access From',
type: 'pie',
radius: ['50%', '100%'],
avoidLabelOverlap: false,
left: '50%',
label: {
show: false,
position: 'center'
},
tooltip: {
show: false
},
emphasis: {
label: {
show: false,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 25, name: '业主', itemStyle: { color: '#0a74fe' } },
{ value: 25, name: '租户', itemStyle: { color: '#fe3d4e' } },
{ value: 25, name: '亲属', itemStyle: { color: '#00cf7f' } },
{ value: 25, name: '其他', itemStyle: { color: '#f48e3b' } }
]
}
]
});
{
'name': '物业费'
},
{
'name': '水电费'
},
{
'name': '商铺租金'
},
{
'name': '维修费'
}
]);
/** 缴费方式 */
const options2 = shallowRef<EChartsOption>({
@@ -216,60 +197,100 @@ const options2 = shallowRef<EChartsOption>({
]
});
// ! 年龄分布 -------------------------------------------------------------------------------------
const agesX = ['6岁以下', '6-11岁', '11-14岁', '15-24岁', '25-30岁', '31-40岁', '41-50岁', '51-61岁', '61-65岁', '65-70岁', '70岁以上'];
const agesData = [520, 680, 460, 750, 1350, 1860, 1420, 960, 630, 380, 210];
// 🔥 关键:灰色背景数据 = 最大值 - 真实数据互补100%
const maxValue = 2000; // 设定总高度(可自行调整)
const middleHeight = 20;
const barwidth = '10%';
const bgData = agesData.map((item) => maxValue - item - middleHeight);
const bgData2 = agesData.map((item) => middleHeight);
/** 年龄分布 */
// 【新增】定义12个月份
const monthX = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
/** 欠费分布 散点图 */
/** 欠费分布 散点图 (带特效版) */
const options3 = shallowRef<EChartsOption>({
title: {
text: '年龄分布',
left: 0,
top: 0,
textStyle: { fontSize: 14, fontWeight: 'normal' }
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `${monthX[params.value[0]]}<br/>欠费金额: <b>${params.value[1]}</b> 万元`;
},
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#eee',
textStyle: { color: '#333' }
},
xAxis: {},
grid: {
left: '3%',
right: '4%',
left: '1%',
right: '2%',
top: '12%',
bottom: '8%',
bottom: '12%',
containLabel: true
},
yAxis: {},
xAxis: {
type: 'category',
data: monthX,
boundaryGap: false,
axisTick: {
alignWithLabel: true,
length: 5, // 刻度线长度
lineStyle: { color: '#ccc' }
},
axisLine: { lineStyle: { color: '#eee' }, onZero: false },
axisLabel: {
color: '#666',
interval: 0, // 强制显示所有月份
margin: 12 // 标签与轴线的距离
},
splitLine: {
show: true,
lineStyle: {
color: '#f0f0f0',
type: 'dashed',
width: 1
}
}
},
yAxis: {
type: 'value',
name: '金额(万元)',
nameTextStyle: { color: '#999', padding: [0, 0, 0, 10] },
splitLine: {
show: true,
lineStyle: {
color: '#f0f0f0',
type: 'dashed' // 虚线网格更轻盈
}
},
axisLabel: { color: '#999', margin: 20 }
},
series: [
{
symbolSize: 20,
name: '欠费金额',
type: 'scatter',
symbolSize: (data: any) => {
const value = data[1];
return Math.min(Math.max(value * 1.5, 10), 40);
},
itemStyle: {
color: (params: any) => {
const index = params.dataIndex;
if (index % 2 === 0) {
return '#739ff0';
} else {
return '#86c9d0';
}
}
},
// 【特效 2】高亮状态鼠标悬停
emphasis: {
scale: 1.2 // 悬停时额外放大
},
data: [
[10.0, 8.04],
[8.07, 6.95],
[13.0, 7.58],
[9.05, 8.81],
[11.0, 8.33],
[14.0, 7.66],
[13.4, 6.81],
[10.0, 6.33],
[14.0, 8.96],
[12.5, 6.82],
[9.15, 7.2],
[11.5, 7.2],
[3.03, 4.23],
[12.2, 7.83],
[2.02, 4.47],
[1.05, 3.33],
[4.05, 4.96],
[6.03, 7.24],
[12.0, 6.26],
[12.0, 8.84],
[7.08, 5.82],
[5.02, 5.68]
],
type: 'scatter'
[0, 12.5],
[1, 10.2],
[2, 15.8],
[3, 9.5],
[4, 11.0],
[5, 18.3],
[6, 14.2],
[7, 16.5],
[8, 13.1],
[9, 10.8],
[10, 12.4],
[11, 19.6]
]
}
]
});
@@ -284,12 +305,6 @@ const outData = [121, 85, 142, 71, 135, 93, 116, 66, 148, 104];
/** 进十日缴费数量情况 - 平滑折线图 */
const flowLineOption = shallowRef<EChartsOption>({
title: {
text: '进十日缴费数量趋势',
left: 0,
top: 0,
textStyle: { fontSize: 14, fontWeight: 'normal' }
},
tooltip: {
trigger: 'axis',
confine: true
@@ -369,11 +384,225 @@ const flowLineOption = shallowRef<EChartsOption>({
]
});
// ! 进十日缴费数量情况 -------------------------------------------------------------------------------------
const paytypelist = ref([
{ label: '微信', value: 'payWx', number: 0, color: '#4a3afe' },
{ label: '支付宝', value: 'payZfb', number: 0, color: '#1e1b39' },
{ label: '线下', value: 'payOffline', number: 0, color: '#c893fd' }
]);
function getlist() {
const startData = getPastDayDate(defaultDay);
income_statsApi(dataTime.value === '' ? startData : dataTime.value).then((res) => {
form.value = res.data;
const { payOffline, payWx, payZfb, feeTypeStats, dailyStats } = res.data;
const total = (payOffline ?? 0) + (payWx ?? 0) + (payZfb ?? 0);
if (total > 0) {
form.value.payOffline = Number.isNaN((payOffline / total) * 100) ? 0 : ((payOffline / total) * 100).toFixed(2);
form.value.payWx = Number.isNaN((payWx / total) * 100) ? 0 : ((payWx / total) * 100).toFixed(2);
form.value.payZfb = Number.isNaN((payZfb / total) * 100) ? 0 : ((payWx / total) * 100).toFixed(2);
paytypelist.value[0].number = form.value.payOffline;
paytypelist.value[1].number = form.value.payWx;
paytypelist.value[2].number = form.value.payZfb;
paytypelist.value.sort((a, b) => Number(b.number) - Number(a.number));
}
// 收入来源
// 2. 处理收入来源 (新逻辑)
if (feeTypeStats && Array.isArray(feeTypeStats)) {
updateIncomeChart(feeTypeStats);
}
// 3. 【新增】处理缴费走势 (近十日缴费情况)
if (dailyStats && Array.isArray(dailyStats)) {
updateFlowLineChart(dailyStats);
}
});
}
/**
* 更新缴费走势图表
* @param stats 后端返回的 dailyStats 数组
*/
function updateFlowLineChart(stats: any[]) {
if (!stats.length) {
flowLineOption.value.series = [];
flowLineOption.value.xAxis.data = [];
return;
}
// 提取数据
const dates = stats.map((item) => item.statDate);
const paidData = stats.map((item) => Number(item.totalPaid) || 0);
const unpaidData = stats.map((item) => Number(item.totalUnpaid) || 0);
// 更新图表配置
// 使用 shallowRef 时,建议替换整个对象或确保深层属性被正确追踪
// 这里我们直接修改 series 和 xAxis 的数据
flowLineOption.value = {
...flowLineOption.value,
xAxis: {
...flowLineOption.value.xAxis,
data: dates
},
series: [
{
name: '已缴费',
type: 'line',
smooth: true,
data: paidData,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#f94144' },
lineStyle: { width: 4 },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: '#fbd7d9' },
{ offset: 1, color: '#fbd7d9' }
]
}
}
},
{
name: '未缴费',
type: 'line',
smooth: true,
data: unpaidData,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#f9c74f' },
lineStyle: { width: 4 },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: '#fcf2dc' },
{ offset: 1, color: '#fcf2dc' }
]
}
}
}
]
};
}
/**
* 更新收入来源图表数组
* @param stats 后端返回的 feeTypeStats 数组
*/
function updateIncomeChart(stats: any[]) {
let statslist = stats;
if (statslist.length === 0) {
incomeChartOptions.value = [];
statslist = [
{
'feeName': '停车费'
},
{
'feeName': '物业费'
},
{
'feeName': '水电费'
},
{
'feeName': '商铺租金'
},
{
'feeName': '维修费'
}
];
}
// 【修改点 2】映射生成多个独立的 ECharts 配置
incomeChartOptions.value = statslist.map((item, index) => {
const percentage = Number(item.paidPercentage) || 0;
const color = '#409eff';
return {
name: item.feeName, // 图表名称
options: {
tooltip: {
show: false,
trigger: 'item',
formatter: '{b}: {c}%'
},
series: [
{
type: 'pie',
radius: ['50%', '70%'], // 环形大小
center: ['50%', '50%'], // 每个图表各自居中
select: {
scale: false
},
labelLine: {
show: false
},
data: [
{
value: 100 - percentage,
itemStyle: {
color: '#ebeef5' // 背景色
},
emphasis: {
itemStyle: {
color: '#ebeef5'
},
scale: false
}
},
{
value: percentage,
name: item.feeName,
itemStyle: { color: color },
label: {
show: true,
position: 'center', // 显示在圆环中心
formatter: '{c}%', // 显示百分比数值
fontSize: 14,
fontWeight: 'bold',
color: '#4ce19c'
},
emphasis: {
scale: true
}
}
]
}
]
}
};
});
console.log(incomeChartOptions.value);
}
function querySearch() {
getlist();
}
function reset() {
dataTime.value = '';
querySearch();
}
onMounted(() => {
open.value = true;
getlist();
});
onUnmounted(() => {
open.value = false;
});
</script>
<style scoped lang="scss">
.personnel {
background: linear-gradient(180deg, #deedff 0%, #f8f9fe 50%);
overflow: auto;
.el-button {
width: 80px;
height: 32px;
@@ -386,7 +615,7 @@ const flowLineOption = shallowRef<EChartsOption>({
width: 100px;
font-size: 0.95rem;
}
// 入住清空
/* 入住清空 */
.cardbox {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, #ffffff 70%);
border-radius: 16px 16px 16px 16px;
@@ -394,11 +623,35 @@ const flowLineOption = shallowRef<EChartsOption>({
font-size: 20px;
padding: 20px;
min-height: 275px;
@media (max-width: 1600px) {
font-size: 18px;
}
@media (max-width: 1300px) {
font-size: 20px;
}
@media (max-width: 1200px) {
margin-bottom: 20px;
}
.card_infoall {
padding-left: 40px;
display: flex;
align-items: center;
gap: 20px;
height: 20px;
line-height: 20px;
color: rgba(51, 51, 51, 1);
font-size: 14px;
margin-top: 15px;
}
.cardbody {
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
.chart-item {
flex-shrink: 0;
}
.card_leftbox {
text-align: center;
.labelbox {
@@ -417,7 +670,7 @@ const flowLineOption = shallowRef<EChartsOption>({
}
.legendbox {
font-size: 0.8rem;
font-size: 15px;
li {
margin-bottom: 20px;
position: relative;
@@ -426,8 +679,8 @@ const flowLineOption = shallowRef<EChartsOption>({
line-height: 50px;
text-align: left;
padding-left: 30px;
&::after {
content: '';
i {
display: block;
position: absolute;
left: 0;
top: 50%;
@@ -436,18 +689,6 @@ const flowLineOption = shallowRef<EChartsOption>({
width: 20px;
height: 20px;
border-radius: 50%;
background: #c893fd;
}
&:first-of-type {
&::after {
background: #4a3aff;
}
}
&:last-of-type {
&::after {
background: #1e1b39;
}
}
}
}
@@ -459,40 +700,55 @@ const flowLineOption = shallowRef<EChartsOption>({
transform: translateY(-40px);
li {
position: absolute;
right: 0;
top: 0;
width: 150px;
height: 150px;
border-radius: 50%;
background-color: #c893fd;
border: 3px solid white;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.4rem;
font-weight: 450;
transition: all 0.3s ease; // 添加过渡动画
// 默认样式 (作为 Level 1 或基础样式)
width: 120px;
height: 120px;
font-size: 1.2rem;
right: 40px;
top: 40px;
z-index: 10;
&:nth-child(1) {
width: 200px;
height: 200px;
right: inherit;
left: 0px;
font-size: 2rem;
top: 30px;
z-index: 9;
background-color: #4a3aff;
background-color: #c893fd; // 默认颜色,会被 data-bg 覆盖
// Level 0: 最大值 (排序后的第一个)
&[data-level='0'] {
width: 180px;
height: 180px;
left: 0;
top: 15px;
right: auto;
font-size: 1.8rem;
z-index: 9; // 层级最低,
}
&:nth-child(3) {
width: 100px;
height: 100px;
left: inherit;
top: inherit;
right: 50px;
font-size: 1.1rem;
bottom: -20px;
z-index: 11;
background-color: #1e1b39;
// Level 1: 中间值
&[data-level='1'] {
width: 130px;
height: 130px;
left: 150px; // 重叠在大球右侧
top: 0;
right: auto;
font-size: 1.3rem;
z-index: 10;
}
// Level 2: 最小值
&[data-level='2'] {
width: 90px;
height: 90px;
left: 150px; // 更靠右
top: 100px; // 靠下
right: auto;
font-size: 1rem;
z-index: 11; // 层级最高,浮在最上面
}
}
}