7/10 缴费分析,人员分析 数据对接
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<script name="RingProgress" setup lang="ts">
|
||||
import { EChartsOption } from 'echarts';
|
||||
import Echarts from '@/components/Echarts/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
value: string;
|
||||
color: string;
|
||||
}>();
|
||||
|
||||
console.log(props.name, props.value);
|
||||
|
||||
const options = ref<EChartsOption>({
|
||||
tooltip: {
|
||||
show: false,
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}%'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '80%'], // 环形大小
|
||||
center: ['50%', '50%'], // 每个图表各自居中
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
|
||||
data: [
|
||||
{
|
||||
value: 100 - Number(props.value),
|
||||
itemStyle: {
|
||||
color: '#ebeef5' // 背景色
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: '#ebeef5'
|
||||
},
|
||||
scale: false
|
||||
}
|
||||
},
|
||||
{
|
||||
value: Number(props.value),
|
||||
name: props.name,
|
||||
itemStyle: { color: props.color, borderRadius: 8, borderColor: props.color, borderWidth: 0 },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center', // 显示在圆环中心
|
||||
formatter: '{c}%', // 显示百分比数值
|
||||
// fontSize: '50%',
|
||||
// fontWeight: 'bold',
|
||||
color: '#4ce19c',
|
||||
rich: {
|
||||
num: {
|
||||
fontSize: '28%', // 富文本内支持百分比,相对圆环直径
|
||||
fontWeight: 'bold',
|
||||
color: '#4ce19c'
|
||||
}
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
scale: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chart-item flex flex-col items-center" style="flex: 1; height: 100%">
|
||||
<Echarts :options="options as EChartsOption" style="width: 100%; height: 165px"></Echarts>
|
||||
<div class="chart-item-name">{{ props.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-item {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.chart-item-name {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -13,47 +13,42 @@
|
||||
<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">
|
||||
<div style="position: relative; z-index: 2">
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="14" :xl="14">
|
||||
<el-col :lg="14" :xl="14" style="margin-bottom: 15px">
|
||||
<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 class="card_InfoAll">
|
||||
<div>应缴费用:{{ formatMoneyWithUnit(sourceFrom.totalPayable) }}</div>
|
||||
<div>已收:{{ formatMoneyWithUnit(sourceFrom.totalReceived) }}</div>
|
||||
<div>待收:{{ formatMoneyWithUnit(sourceFrom.totalPending) }}</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 ?? []" style="width: 100%; height: 165px"></Echarts>
|
||||
<!-- 如果需要额外显示名称,可以在这里加 div,但图表中心已显示 -->
|
||||
<div class="chart-item-name">{{ opt.name }}</div>
|
||||
</div>
|
||||
<div class="cardBody" style="display: flex; flex-wrap: wrap" v-if="sourceFromIsComputed">
|
||||
<RingProgress name="停车费" color="#1a75ff" :value="sourceFrom.parkingFeePercent"></RingProgress>
|
||||
<RingProgress name="物业费" color="#ffbf05" :value="sourceFrom.propertyFeePercent"></RingProgress>
|
||||
<RingProgress name="水电费" color="#6c76f4" :value="sourceFrom.utilityFeePercent"></RingProgress>
|
||||
<RingProgress name="商铺租金" color="#fa746b" :value="sourceFrom.shopRentPercent"></RingProgress>
|
||||
<RingProgress name="维修费" color="#3fdf95" :value="sourceFrom.repairFeePercent"></RingProgress>
|
||||
<RingProgress name="垃圾处理费" color="#1e1b39" :value="sourceFrom.garbageFeePercent"></RingProgress>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="10" :xl="10">
|
||||
<el-col :lg="10" :xl="10" style="margin-bottom: 15px">
|
||||
<div class="cardbox">
|
||||
<div>欠费分布</div>
|
||||
<div class="cardbody w-full" style="height: 150px">
|
||||
<div>缴费方式</div>
|
||||
<div class="cardBody w-full" style="height: 100%">
|
||||
<Echarts :options="options2"></Echarts>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div style="margin-bottom: 15px; position: relative; z-index: 2">
|
||||
<div style="position: relative; z-index: 2">
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="12" :xl="14">
|
||||
<el-col :lg="12" :xl="14" style="margin-bottom: 15px">
|
||||
<div class="cardbox w-full">
|
||||
<div>欠费分布</div>
|
||||
<div class="cardbody w-full" style="height: 330px">
|
||||
<div class="cardBody w-full" style="height: 330px">
|
||||
<Echarts :options="options3"></Echarts>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +67,7 @@
|
||||
<span class="ml-2">{{ item.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="circlebox">
|
||||
<ul class="circleBox">
|
||||
<li :data-level="index" :style="{ backgroundColor: item.color }" v-for="(item, index) in sortPayList" :key="index">
|
||||
{{ item.number }}%
|
||||
</li>
|
||||
@@ -90,7 +85,7 @@
|
||||
</div> -->
|
||||
<div class="cardbox w-full">
|
||||
<div>缴费走势分析</div>
|
||||
<div class="cardbody w-full" style="height: 400px">
|
||||
<div class="cardBody w-full" style="height: 400px">
|
||||
<Echarts :options="flowLineOption"></Echarts>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,59 +105,48 @@ import Echarts from '@/components/Echarts/index.vue';
|
||||
import { formatMoneyWithUnit } from '@/utils/money';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { getLast7Days, getPastDayDate } from '@/utils/time';
|
||||
import { income_statsApi } from '@/api/system/Payment';
|
||||
|
||||
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': []
|
||||
});
|
||||
import {
|
||||
arrears_distributionApi,
|
||||
Arrears_distrubution,
|
||||
income_scourcApi,
|
||||
payment_methodApi,
|
||||
payment_sourceApi,
|
||||
payment_trendApi,
|
||||
payment_trendType,
|
||||
PayMentMethod
|
||||
} from '@/api/system/Payment';
|
||||
import RingProgress from './compontents/RingProgress.vue';
|
||||
|
||||
const defaultDay = 30; // 默认查询近30天数据
|
||||
const open = ref(false);
|
||||
const last10Day = getPastDayDate(defaultDay);
|
||||
const dataTime = ref(last10Day);
|
||||
|
||||
const sourceFromIsComputed = ref(true);
|
||||
/** 收入来源 */
|
||||
/** 收入来源图表配置 */
|
||||
const incomeChartOptions = ref<
|
||||
{
|
||||
name: string;
|
||||
options?: EChartsOption;
|
||||
}[]
|
||||
>(
|
||||
[
|
||||
{
|
||||
'name': '停车费'
|
||||
},
|
||||
{
|
||||
'name': '物业费'
|
||||
},
|
||||
{
|
||||
'name': '水电费'
|
||||
},
|
||||
{
|
||||
'name': '商铺租金'
|
||||
},
|
||||
{
|
||||
'name': '维修费'
|
||||
}
|
||||
].map((i) => {
|
||||
return {
|
||||
name: i.name,
|
||||
options: {
|
||||
tooltip: {},
|
||||
series: []
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
const sourceFrom = ref({
|
||||
'totalPayable': '0',
|
||||
'totalReceived': '0',
|
||||
'totalPending': '0',
|
||||
|
||||
'parkingFee': '0',
|
||||
'parkingFeePercent': '0',
|
||||
|
||||
'propertyFee': '0',
|
||||
'propertyFeePercent': '0',
|
||||
|
||||
'utilityFee': '0',
|
||||
'utilityFeePercent': '0',
|
||||
|
||||
'shopRent': '0',
|
||||
'shopRentPercent': '0',
|
||||
|
||||
'repairFee': '0',
|
||||
'repairFeePercent': '0',
|
||||
|
||||
'garbageFee': '0',
|
||||
'garbageFeePercent': '0'
|
||||
});
|
||||
|
||||
/** 缴费方式 */
|
||||
const options2 = shallowRef<EChartsOption>({
|
||||
@@ -199,23 +183,54 @@ const options2 = shallowRef<EChartsOption>({
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: 50, name: '支付宝', itemStyle: { color: '#0a74fe' } },
|
||||
{ value: 30, name: '微信', itemStyle: { color: '#f74043' } },
|
||||
{ value: 20, name: '线下', itemStyle: { color: '#fcd152' } }
|
||||
{ value: 0, name: '支付宝', itemStyle: { color: '#0a74fe' } },
|
||||
{ value: 0, name: '微信', itemStyle: { color: '#f74043' } },
|
||||
{ value: 0, name: '线下', itemStyle: { color: '#fcd152' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
function updatePayMethod(dataFrom: PayMentMethod): EChartsOption {
|
||||
return {
|
||||
legend: {
|
||||
top: 'center',
|
||||
left: '0',
|
||||
orient: 'vertical',
|
||||
itemGap: 50,
|
||||
itemWidth: 12, // 图例色块宽度
|
||||
itemHeight: 12 // 图例色块高度
|
||||
},
|
||||
clockwise: true,
|
||||
series: [
|
||||
{
|
||||
name: '缴费方式',
|
||||
type: 'pie',
|
||||
startAngle: 280,
|
||||
radius: ['50%', '100%'],
|
||||
avoidLabelOverlap: false,
|
||||
left: '50%',
|
||||
emphasis: {
|
||||
scale: true,
|
||||
scaleSize: 6
|
||||
},
|
||||
data: [
|
||||
{ value: Number(dataFrom.alipayAmount) ?? 0, name: '支付宝', itemStyle: { color: '#0a74fe' } },
|
||||
{ value: Number(dataFrom.wechatAmount) ?? 0, name: '微信', itemStyle: { color: '#f74043' } },
|
||||
{ value: Number(dataFrom.offlineAmount) ?? 0, name: '线下', itemStyle: { color: '#fcd152' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 【新增】定义12个月份
|
||||
const monthX = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
|
||||
/** 欠费分布 散点图 */
|
||||
/** 欠费分布 散点图 (带特效版) */
|
||||
const options3 = shallowRef<EChartsOption>({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
return `${monthX[params.value[0]]}<br/>欠费金额: <b>${params.value[1]}</b> 万元`;
|
||||
return `${params.name}<br/>欠费金额: <b>${formatMoneyWithUnit(params.value[1])}</b>`;
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#eee',
|
||||
@@ -304,7 +319,92 @@ const options3 = shallowRef<EChartsOption>({
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
function UpdateOptions3(data: Arrears_distrubution[]): EChartsOption {
|
||||
const xData = data.map((item, index) => {
|
||||
return [index, item.amount];
|
||||
});
|
||||
const xMonth = data.map((item) => `${item.month}月`);
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
return `${params.name}<br/>欠费金额: <b>${formatMoneyWithUnit(params.value[1])}</b>`;
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: '#eee',
|
||||
textStyle: { color: '#333' }
|
||||
},
|
||||
grid: {
|
||||
left: '1%',
|
||||
right: '2%',
|
||||
top: '12%',
|
||||
bottom: '12%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xMonth,
|
||||
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: [
|
||||
{
|
||||
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: xData
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
// ! 进十日缴费数量情况 -------------------------------------------------------------------------------------
|
||||
// 近10天日期
|
||||
const dayX = getLast7Days(10);
|
||||
@@ -393,50 +493,65 @@ 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' }
|
||||
const payTypeList = ref([
|
||||
{ label: '小程序', number: 0, color: '#4a3afe' },
|
||||
{ label: '线下缴费', number: 0, color: '#1e1b39' },
|
||||
{ label: '手机APP', number: 0, color: '#c893fd' }
|
||||
]);
|
||||
// 排序后
|
||||
const sortPayList = ref([]);
|
||||
const sortPayList = ref(payTypeList.value);
|
||||
|
||||
const payMethodFrom = ref({
|
||||
'wechatAmount': '0',
|
||||
'wechatPercent': '0',
|
||||
'alipayAmount': '0',
|
||||
'alipayPercent': '0',
|
||||
'offlineAmount': '0',
|
||||
'offlinePercent': '0'
|
||||
});
|
||||
|
||||
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 : ((payZfb / total) * 100).toFixed(2);
|
||||
const payTypeList = JSON.parse(JSON.stringify(paytypelist.value));
|
||||
payTypeList[0].number = form.value.payWx;
|
||||
payTypeList[1].number = form.value.payZfb;
|
||||
payTypeList[2].number = form.value.payOffline;
|
||||
sortPayList.value = payTypeList.toSorted((a, b) => Number(b.number) - Number(a.number));
|
||||
}
|
||||
|
||||
// 收入来源
|
||||
// 2. 处理收入来源 (新逻辑)
|
||||
if (feeTypeStats && Array.isArray(feeTypeStats)) {
|
||||
updateIncomeChart(feeTypeStats);
|
||||
}
|
||||
|
||||
// 3. 【新增】处理缴费走势 (近十日缴费情况)
|
||||
if (dailyStats && Array.isArray(dailyStats)) {
|
||||
updateFlowLineChart(dailyStats);
|
||||
}
|
||||
sourceFromIsComputed.value = false;
|
||||
// 收入来源
|
||||
income_scourcApi(dataTime.value)
|
||||
.then((res) => {
|
||||
sourceFrom.value = res.data;
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
sourceFromIsComputed.value = true;
|
||||
});
|
||||
});
|
||||
// 缴费方式
|
||||
payment_methodApi(dataTime.value).then((res) => {
|
||||
payMethodFrom.value = res.data;
|
||||
options2.value = updatePayMethod(res.data);
|
||||
});
|
||||
// 欠费分布
|
||||
arrears_distributionApi(dataTime.value).then((res) => {
|
||||
options3.value = UpdateOptions3(res.data.items);
|
||||
});
|
||||
//缴费来源
|
||||
payment_sourceApi(dataTime.value).then((res) => {
|
||||
const payTypeListCopy = JSON.parse(JSON.stringify(payTypeList.value));
|
||||
payTypeListCopy[0].number = res.data.miniProgramPercent;
|
||||
payTypeListCopy[1].number = res.data.offlinePercent;
|
||||
payTypeListCopy[2].number = res.data.mobileAppPercent;
|
||||
sortPayList.value = payTypeListCopy.toSorted(
|
||||
(a: { number: string | number }, b: { number: string | number }) => Number(b.number) - Number(a.number)
|
||||
);
|
||||
});
|
||||
// 缴费走势
|
||||
payment_trendApi(dataTime.value, getPastDayDate(0)).then((res) => {
|
||||
updateFlowLineChart(res.data.items);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 更新缴费走势图表
|
||||
* @param stats 后端返回的 dailyStats 数组
|
||||
* @param stats 后端返回的 dailyStats 数组&
|
||||
*/
|
||||
function updateFlowLineChart(stats: any[]) {
|
||||
function updateFlowLineChart(stats: payment_trendType[]) {
|
||||
if (!stats.length) {
|
||||
flowLineOption.value.series = [];
|
||||
flowLineOption.value.xAxis.data = [];
|
||||
@@ -444,13 +559,13 @@ function updateFlowLineChart(stats: any[]) {
|
||||
}
|
||||
|
||||
// 提取数据
|
||||
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);
|
||||
const dates = stats.map((item) => item.date);
|
||||
const paidData = stats.map((item) => Number(item.paidAmount) || 0);
|
||||
const unpaidData = stats.map((item) => Number(item.unpaidAmount) || 0);
|
||||
|
||||
// 更新图表配置
|
||||
// 使用 shallowRef 时,建议替换整个对象或确保深层属性被正确追踪
|
||||
// 这里我们直接修改 series 和 xAxis 的数据
|
||||
// 这里我们直接修改 series 和 xAxis 的数据 ╬╬
|
||||
flowLineOption.value = {
|
||||
...flowLineOption.value,
|
||||
xAxis: {
|
||||
@@ -507,96 +622,12 @@ function updateFlowLineChart(stats: any[]) {
|
||||
]
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 更新收入来源图表数组
|
||||
* @param stats 后端返回的 feeTypeStats 数组
|
||||
*/
|
||||
function updateIncomeChart(stats: any[]) {
|
||||
incomeChartOptions.value = [];
|
||||
let statslist = stats;
|
||||
if (statslist.length === 0) {
|
||||
statslist = [
|
||||
{
|
||||
'feeName': '停车费'
|
||||
},
|
||||
{
|
||||
'feeName': '物业费'
|
||||
},
|
||||
{
|
||||
'feeName': '水电费'
|
||||
},
|
||||
{
|
||||
'feeName': '商铺租金'
|
||||
},
|
||||
{
|
||||
'feeName': '维修费'
|
||||
}
|
||||
];
|
||||
}
|
||||
incomeChartOptions.value = statslist.map((item) => {
|
||||
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 = '';
|
||||
dataTime.value = getPastDayDate(defaultDay);
|
||||
querySearch();
|
||||
}
|
||||
getList();
|
||||
@@ -632,6 +663,7 @@ onUnmounted(() => {
|
||||
font-size: 20px;
|
||||
padding: 20px;
|
||||
min-height: 275px;
|
||||
height: 100%;
|
||||
@media (max-width: 1600px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -641,7 +673,7 @@ onUnmounted(() => {
|
||||
@media (max-width: 1200px) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card_infoall {
|
||||
.card_InfoAll {
|
||||
padding-left: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -653,7 +685,7 @@ onUnmounted(() => {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cardbody {
|
||||
.cardBody {
|
||||
padding: 0 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -663,10 +695,10 @@ onUnmounted(() => {
|
||||
}
|
||||
.card_leftbox {
|
||||
text-align: center;
|
||||
.labelbox {
|
||||
.labelBox {
|
||||
font-size: 16px;
|
||||
}
|
||||
.valuebox {
|
||||
.valueBox {
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
color: #222222;
|
||||
@@ -683,7 +715,6 @@ onUnmounted(() => {
|
||||
li {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: left;
|
||||
@@ -700,7 +731,7 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.circlebox {
|
||||
.circleBox {
|
||||
margin-left: 60px;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<el-col :lg="8" :xl="8">
|
||||
<div class="cardbox">
|
||||
<div>入住情况</div>
|
||||
<div class="cardbody" style="height: 200px">
|
||||
<div class="card_leftbox">
|
||||
<div class="valuebox">{{ movein.totalResident }}</div>
|
||||
<div class="labelbox">入住人数</div>
|
||||
<div class="cardBody" style="height: 200px">
|
||||
<div class="card_LeftBox">
|
||||
<div class="valueBox">{{ MoveIn.totalResident }}</div>
|
||||
<div class="labelBox">入住人数</div>
|
||||
</div>
|
||||
<div class="echartsbox h-full">
|
||||
<Echarts :options="options"></Echarts>
|
||||
<div class="echartsBox h-full">
|
||||
<Echarts :options="options as EChartsOption"></Echarts>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,16 +31,16 @@
|
||||
<el-col :lg="10" :xl="8">
|
||||
<div class="cardbox">
|
||||
<div>住户类型情况</div>
|
||||
<div class="cardbody w-full" style="height: 150px">
|
||||
<Echarts :options="options1"></Echarts>
|
||||
<div class="cardBody w-full" style="height: 180px">
|
||||
<Echarts :options="options1 as EChartsOption"></Echarts>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :lg="6" :xl="8">
|
||||
<div class="cardbox">
|
||||
<div>男女比例</div>
|
||||
<div class="cardbody w-full" style="height: 150px">
|
||||
<Echarts :options="options2"></Echarts>
|
||||
<div class="cardBody w-full" style="height: 180px">
|
||||
<Echarts :options="options2 as EChartsOption"></Echarts>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -56,7 +56,7 @@
|
||||
<el-row style="margin-top: 20px; position: relative; z-index: 2">
|
||||
<el-col :span="24">
|
||||
<div class="cardbox w-full" style="height: 450px">
|
||||
<Echarts :options="flowLineOption"></Echarts>
|
||||
<Echarts :options="flowLineOption as EChartsOption"></Echarts>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -68,12 +68,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO 人员分析
|
||||
import { ref } from 'vue';
|
||||
import Echarts from '@/components/Echarts/index.vue';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { getLastNDays } from '@/utils/time';
|
||||
import { get_movein_moveout_list, get_movein_user_sex, get_movein_user_type, user_movein_list } from '@/api/system/Personnel';
|
||||
import { get_movein_moveout_list, get_movein_user_sex, get_movein_user_type, get_person_age, user_movein_list } from '@/api/system/Personnel';
|
||||
|
||||
const date = ref('');
|
||||
const open = ref(true);
|
||||
@@ -86,25 +85,29 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
function reset() {
|
||||
date.value = '';
|
||||
date.value = getLastNDays(10)[0];
|
||||
querySelect();
|
||||
}
|
||||
|
||||
/** 入住情况 */
|
||||
const options = ref<EChartsOption>({
|
||||
tooltip: { show: false },
|
||||
tooltip: { show: true, trigger: 'item' },
|
||||
series: [
|
||||
// 1. 底层细圆环(灰色底色)
|
||||
{
|
||||
name: '总房屋',
|
||||
type: 'pie',
|
||||
radius: ['68%', '90%'], // 细一点
|
||||
clockwise: true,
|
||||
data: [100],
|
||||
itemStyle: { color: '#ebeef5' },
|
||||
emphasis: { disabled: true },
|
||||
tooltip: { show: false }, // 关键:不显示提示
|
||||
label: { show: false }
|
||||
},
|
||||
// 2. 上层粗圆环(进度条)
|
||||
{
|
||||
name: '入住房屋',
|
||||
type: 'pie',
|
||||
radius: ['65%', '98%'], // 粗一点
|
||||
clockwise: true,
|
||||
@@ -132,33 +135,39 @@ const options = ref<EChartsOption>({
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [{ value: 85 }, { value: 15, itemStyle: { color: 'transparent' } }]
|
||||
data: [
|
||||
{ value: 85 },
|
||||
{
|
||||
value: 15,
|
||||
itemStyle: { color: 'transparent' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/** 入住类型 */
|
||||
/** 住户类型 */
|
||||
const options1 = ref<EChartsOption>({
|
||||
legend: {
|
||||
top: 'center',
|
||||
left: '0',
|
||||
orient: 'vertical',
|
||||
itemGap: 50
|
||||
itemGap: 80
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
name: '住户类型',
|
||||
type: 'pie',
|
||||
radius: ['50%', '100%'],
|
||||
radius: ['45%', '95%'],
|
||||
avoidLabelOverlap: false,
|
||||
left: '50%',
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
@@ -185,12 +194,15 @@ const options2 = ref<EChartsOption>({
|
||||
top: 'center',
|
||||
left: '0',
|
||||
orient: 'vertical',
|
||||
itemGap: 50
|
||||
itemGap: 80
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
clockwise: true,
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
name: '男女比例',
|
||||
type: 'pie',
|
||||
startAngle: 280,
|
||||
radius: ['50%', '100%'],
|
||||
@@ -200,9 +212,6 @@ const options2 = ref<EChartsOption>({
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
@@ -221,15 +230,23 @@ const options2 = ref<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);
|
||||
const agesData = ref(new Array(11).fill(0));
|
||||
const maxValue = ref(10); // 设定总高度
|
||||
const middleHeight = 0.05; // 中间白条高度
|
||||
const barWidth = '10%'; // 柱状图粗度
|
||||
const bgData = computed(() => {
|
||||
return agesData.value.map((item) => {
|
||||
const num = maxValue.value - item - middleHeight;
|
||||
if (num > 0) {
|
||||
return num;
|
||||
} else {
|
||||
return maxValue.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
const bgData2 = agesX.map(() => middleHeight);
|
||||
/** 年龄分布 */
|
||||
const options3 = shallowRef<EChartsOption>({
|
||||
title: {
|
||||
@@ -250,19 +267,16 @@ const options3 = shallowRef<EChartsOption>({
|
||||
confine: true,
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
// 鼠标悬浮高亮当前,其他隐藏
|
||||
|
||||
xAxis: { type: 'category', data: agesX, axisLabel: { interval: 0 } },
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { show: true, lineStyle: { color: '#eee' } }
|
||||
},
|
||||
series: [
|
||||
// 蓝色真实数据
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: barwidth,
|
||||
data: agesData,
|
||||
barWidth: barWidth,
|
||||
data: agesData.value,
|
||||
itemStyle: {
|
||||
color: '#409eff',
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
@@ -271,7 +285,7 @@ const options3 = shallowRef<EChartsOption>({
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: barwidth,
|
||||
barWidth: barWidth,
|
||||
data: bgData2,
|
||||
tooltip: { show: false }, // 关键:不显示提示
|
||||
itemStyle: {
|
||||
@@ -281,8 +295,8 @@ const options3 = shallowRef<EChartsOption>({
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: barwidth,
|
||||
data: bgData,
|
||||
barWidth: barWidth,
|
||||
data: bgData.value,
|
||||
tooltip: { show: false }, // 关键:不显示提示
|
||||
itemStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)'
|
||||
@@ -291,6 +305,65 @@ const options3 = shallowRef<EChartsOption>({
|
||||
}
|
||||
]
|
||||
});
|
||||
function updateOptions3(data: number[]): EChartsOption {
|
||||
return {
|
||||
title: {
|
||||
text: '住户年龄分布',
|
||||
left: '0',
|
||||
top: '0',
|
||||
textStyle: { fontSize: 16, fontWeight: 'bold' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
top: '12%',
|
||||
bottom: '8%',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
confine: true,
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
xAxis: { type: 'category', data: agesX, axisLabel: { interval: 0 } },
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { show: true, lineStyle: { color: '#eee' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: barWidth,
|
||||
data: data,
|
||||
itemStyle: {
|
||||
color: '#409eff',
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
stack: 'total'
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: barWidth,
|
||||
data: bgData2,
|
||||
tooltip: { show: false }, // 关键:不显示提示
|
||||
itemStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
stack: 'total'
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: barWidth,
|
||||
data: bgData.value,
|
||||
tooltip: { show: false }, // 关键:不显示提示
|
||||
itemStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)'
|
||||
},
|
||||
stack: 'total'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
// ! 年龄分布 -------------------------------------------------------------------------------------
|
||||
|
||||
// ! 人员流动情况 -------------------------------------------------------------------------------------
|
||||
@@ -388,7 +461,7 @@ const flowLineOption = ref<EChartsOption>({
|
||||
}
|
||||
]
|
||||
});
|
||||
function flowLineOptionFun(days: string[], indata: number[], outdata: number[]): EChartsOption {
|
||||
function flowLineOptionFun(days: string[], InData: number[], OutData: number[]): EChartsOption {
|
||||
return {
|
||||
title: {
|
||||
text: '近十日人员流动趋势',
|
||||
@@ -430,7 +503,7 @@ function flowLineOptionFun(days: string[], indata: number[], outdata: number[]):
|
||||
name: '人员流入',
|
||||
type: 'line',
|
||||
smooth: true, // 平滑折线 核心
|
||||
data: indata,
|
||||
data: InData,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: '#f94144' },
|
||||
@@ -453,7 +526,7 @@ function flowLineOptionFun(days: string[], indata: number[], outdata: number[]):
|
||||
name: '人员流出',
|
||||
type: 'line',
|
||||
smooth: true, // 平滑折线 核心
|
||||
data: outdata,
|
||||
data: OutData,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: '#f9c74f' },
|
||||
@@ -477,34 +550,43 @@ function flowLineOptionFun(days: string[], indata: number[], outdata: number[]):
|
||||
}
|
||||
// ! 人员流动情况 -------------------------------------------------------------------------------------
|
||||
|
||||
const movein = ref({
|
||||
const MoveIn = ref({
|
||||
totalResident: 0,
|
||||
totalHouse: 0
|
||||
});
|
||||
function init() {
|
||||
// 获取入住情况
|
||||
moveinFun();
|
||||
// 入住情况
|
||||
MoveInFun();
|
||||
// 入住类型
|
||||
moveinUserType();
|
||||
|
||||
moveinUserSex();
|
||||
moveinUserMoveinOut();
|
||||
MoveInUserType();
|
||||
// 男女类型
|
||||
MoveInUserSex();
|
||||
// 迁入迁出
|
||||
MoveInUserMoveInOut();
|
||||
// 住户年龄分布
|
||||
MoveInPersonAge();
|
||||
}
|
||||
init();
|
||||
function querySelect() {
|
||||
init();
|
||||
}
|
||||
|
||||
function moveinFun() {
|
||||
// 入住情况
|
||||
function MoveInFun() {
|
||||
user_movein_list({
|
||||
datetime: date.value
|
||||
dateTime: date.value
|
||||
}).then((res) => {
|
||||
// 获取入住情况
|
||||
movein.value.totalResident = res.data.occupancyCount;
|
||||
movein.value.totalHouse = res.data.totalHouse;
|
||||
MoveIn.value.totalResident = res.data.occupancyCount;
|
||||
MoveIn.value.totalHouse = res.data.totalHouse;
|
||||
options.value.series[1].data = [
|
||||
{ value: res.data.occupiedHouse },
|
||||
{ value: res.data.totalHouse - res.data.occupiedHouse, itemStyle: { color: 'transparent' } }
|
||||
{
|
||||
value: res.data.totalHouse - res.data.occupiedHouse,
|
||||
itemStyle: { color: 'transparent' },
|
||||
tooltip: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
];
|
||||
options.value.series[1].label.formatter = () => {
|
||||
const total = res.data.totalHouse;
|
||||
@@ -518,17 +600,22 @@ function moveinFun() {
|
||||
});
|
||||
}
|
||||
// 获取住户类型
|
||||
function moveinUserType() {
|
||||
function MoveInUserType() {
|
||||
get_movein_user_type({
|
||||
datetime: date.value
|
||||
dateTime: date.value
|
||||
}).then((res) => {
|
||||
console.log(res);
|
||||
options1.value.series[0].data = [
|
||||
{ value: res.owner, name: '业主', itemStyle: { color: '#0a74fe' } },
|
||||
{ value: res.tenant, name: '租户', itemStyle: { color: '#fe3d4e' } },
|
||||
{ value: res.relatives, name: '亲属', itemStyle: { color: '#00cf7f' } },
|
||||
{ value: res.other, name: '其他', itemStyle: { color: '#f48e3b' } }
|
||||
];
|
||||
});
|
||||
}
|
||||
// 住户性别
|
||||
function moveinUserSex() {
|
||||
function MoveInUserSex() {
|
||||
get_movein_user_sex({
|
||||
datetime: date.value
|
||||
dateTime: date.value
|
||||
}).then((res) => {
|
||||
options2.value.series[0].data = [
|
||||
{ value: res.man, name: '男', itemStyle: { color: '#0a74fe' } },
|
||||
@@ -537,11 +624,7 @@ function moveinUserSex() {
|
||||
});
|
||||
}
|
||||
// 迁入迁出
|
||||
function moveinUserMoveinOut() {
|
||||
// 判断 date.value 是否为今天
|
||||
if (date.value === '') {
|
||||
date.value = getLastNDays(7)[0];
|
||||
}
|
||||
function MoveInUserMoveInOut() {
|
||||
get_movein_moveout_list({
|
||||
date: date.value
|
||||
}).then((res) => {
|
||||
@@ -551,6 +634,17 @@ function moveinUserMoveinOut() {
|
||||
flowLineOption.value = flowLineOptionFun(dayX.value, inData.value, outData.value);
|
||||
});
|
||||
}
|
||||
// 住户年龄分布
|
||||
function MoveInPersonAge() {
|
||||
get_person_age({
|
||||
dateTime: date.value
|
||||
}).then((res) => {
|
||||
const values = Object.values(res).slice(0, -1);
|
||||
maxValue.value = res.total;
|
||||
agesData.value = values;
|
||||
options3.value = updateOptions3(values);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -571,6 +665,7 @@ function moveinUserMoveinOut() {
|
||||
}
|
||||
// 入住清空
|
||||
.cardbox {
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, #ffffff 70%);
|
||||
border-radius: 16px 16px 16px 16px;
|
||||
border: 2px solid #ffffff;
|
||||
@@ -580,25 +675,25 @@ function moveinUserMoveinOut() {
|
||||
@media (max-width: 1200px) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.cardbody {
|
||||
.cardBody {
|
||||
padding: 0 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.card_leftbox {
|
||||
.card_LeftBox {
|
||||
text-align: center;
|
||||
margin-right: 20px;
|
||||
.labelbox {
|
||||
.labelBox {
|
||||
font-size: 16px;
|
||||
}
|
||||
.valuebox {
|
||||
.valueBox {
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
color: #222222;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
.echartsbox {
|
||||
.echartsBox {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user