Files
admin-vben5/apps/web-antd/src/views/property/greenPlantRentalManagement/reportStatistics/index.vue

498 lines
14 KiB
Vue
Raw Normal View History

2025-06-26 18:02:43 +08:00
<script setup lang="ts">
2025-06-27 18:03:13 +08:00
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Button,Radio,Spin } from 'ant-design-vue';
2025-07-09 18:18:48 +08:00
import type { RadioChangeEvent } from 'ant-design-vue';
2025-06-26 18:02:43 +08:00
import { statisticsByTime,
countByRentalType,
countByCusType,
countRenewRate,
countByCusScore,
countOrderAndAmount,
2025-07-09 18:18:48 +08:00
countCustomers,
countAchievedRate,
countAchieved
} from '#/api/property/reportStatistics';
2025-07-05 09:11:29 +08:00
2025-06-26 18:02:43 +08:00
const orderLineRef = ref<EchartsUIType>();
const leasePieRef = ref<EchartsUIType>();
const customerTypesBarRef = ref<EchartsUIType>();
const customerRenewalLineRef = ref<EchartsUIType>();
const conservationTasksBarRef = ref<EchartsUIType>();
const maintenanceQualityScoresPeiRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(orderLineRef);
const { renderEcharts: renderLeasePie } = useEcharts(leasePieRef);
2025-06-27 18:03:13 +08:00
const { renderEcharts: renderCustomerTypesBar } =
useEcharts(customerTypesBarRef);
const { renderEcharts: renderCustomerRenewalLine } = useEcharts(
customerRenewalLineRef,
);
const { renderEcharts: renderConservationTasksBar } = useEcharts(
conservationTasksBarRef,
);
const { renderEcharts: renderMaintenanceQualityScoresPei } = useEcharts(
maintenanceQualityScoresPeiRef,
);
2025-07-05 09:11:29 +08:00
const timeUnit = ref<number>(1)
2025-06-26 18:02:43 +08:00
const countOrderAndAmountDataAmount = ref<number>(0);
const countOrderAndAmountDataOrder = ref<number>(0);
const countAchievedRateData = ref<any>(null);
const countCustomersData = ref<any>(0);
2025-07-09 18:18:48 +08:00
const xAxisData = ref<any[]>([]);
const seriesData = ref<any[]>([]);
const loading = ref(false);
async function fetchOrderAndAmount() {
const countOrderAndAmountData = await countOrderAndAmount();
countOrderAndAmountDataAmount.value = countOrderAndAmountData.amount;
countOrderAndAmountDataOrder.value = countOrderAndAmountData.num;
}
async function fetchCustomers() {
2025-07-09 18:18:48 +08:00
const countCustomersDataRes: any = await countCustomers();
countCustomersData.value = countCustomersDataRes.count;
}
async function fetchAchievedRate() {
const countAchievedRateDataRes: any = await countAchievedRate();
countAchievedRateData.value = countAchievedRateDataRes.rate;
}
async function fetchOrderTrend() {
2025-07-05 09:11:29 +08:00
const res = await statisticsByTime({ timeUnit: timeUnit.value });
2025-07-09 18:18:48 +08:00
xAxisData.value = res?.time ?? [];
seriesData.value = res?.counts ?? [];
2025-06-26 18:02:43 +08:00
renderEcharts({
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
2025-07-09 18:18:48 +08:00
data: xAxisData.value,
2025-06-26 18:02:43 +08:00
boundaryGap: false,
},
yAxis: { type: 'value', axisLabel: { formatter: (value) => `${value * 100}%` } },
2025-06-26 18:02:43 +08:00
series: [
{
2025-07-10 17:52:54 +08:00
name: '订单趋势',
2025-06-26 18:02:43 +08:00
type: 'line',
data: seriesData.value || [],
2025-06-26 18:02:43 +08:00
smooth: true,
},
],
});
}
async function fetchLeasePie() {
const data = await countByRentalType();
const convertedData = data.map((item: { amount: number; type: string }) => ({
value: item.amount,
name: item.type,
}));
2025-06-26 18:02:43 +08:00
renderLeasePie({
title: { text: '租赁金额分布', left: 'center' },
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: 'left' },
series: [
{
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
data: convertedData || [],
2025-06-26 18:02:43 +08:00
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
label: {
formatter: '{b}: {c} ({d}%)',
show: true,
},
},
],
});
}
async function fetchCustomerTypesBar() {
const countByCusTypeData: any = await countByCusType();
2025-06-26 18:02:43 +08:00
renderCustomerTypesBar({
2025-07-10 17:52:54 +08:00
title: { text: '客户类型分布' },
2025-06-26 18:02:43 +08:00
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: ['企业客户', '个人客户', '政府机构', '商业地产', '其他'],
boundaryGap: true,
2025-06-26 18:02:43 +08:00
},
yAxis: { type: 'value' },
2025-06-26 18:02:43 +08:00
series: [
{
2025-07-10 17:52:54 +08:00
name: '客户数',
2025-06-26 18:02:43 +08:00
type: 'bar',
data: countByCusTypeData.counts || [],
2025-06-26 18:02:43 +08:00
},
],
});
}
async function fetchCustomerRenewalLine() {
const countRenewRateData: any = await countRenewRate();
2025-06-26 18:02:43 +08:00
renderCustomerRenewalLine({
2025-06-27 18:03:13 +08:00
title: { text: '客户续租率趋势' },
tooltip: {
trigger: 'axis',
formatter: function (params: any) {
2025-07-10 17:52:54 +08:00
let result = params[0].axisValue + '<br/>';
params.forEach((item: any) => {
2025-07-10 17:52:54 +08:00
result += item.marker + item.seriesName + '' + item.data + '%<br/>';
});
return result;
},
2025-07-10 17:52:54 +08:00
},
2025-06-26 18:02:43 +08:00
xAxis: {
type: 'category',
data: countRenewRateData.month || [],
2025-06-26 18:02:43 +08:00
boundaryGap: false,
},
2025-07-10 17:52:54 +08:00
yAxis: {
type: 'value',
axisLabel: { formatter: '{value}%' },
},
2025-06-26 18:02:43 +08:00
series: [
{
2025-07-10 17:52:54 +08:00
name: '续租率',
2025-06-26 18:02:43 +08:00
type: 'line',
data: countRenewRateData.rate || [],
2025-06-26 18:02:43 +08:00
smooth: true,
},
],
});
}
async function fetchConservationTasksBar() {
const countAchievedData: any = await countAchieved();
2025-06-26 18:02:43 +08:00
renderConservationTasksBar({
2025-06-27 18:03:13 +08:00
title: { text: '养护任务完成情况' },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function (params: any) {
let result = params[0].axisValue + '<br/>';
params.forEach((item: any) => {
if (item.seriesName === '完成率') {
result += item.marker + item.seriesName + '' + item.data + '%<br/>';
} else {
result += item.marker + item.seriesName + '' + item.data + '<br/>';
}
});
return result;
},
2025-06-26 18:02:43 +08:00
},
legend: { data: ['计划任务数', '已完成数', '完成率'] },
2025-06-27 18:03:13 +08:00
xAxis: [
{
type: 'category',
data: ['修剪整形', '肥水管理', '中耕除草', '病虫害防治', '越冬防寒'],
2025-06-27 18:03:13 +08:00
},
],
yAxis: [
{ type: 'value', name: '任务数', min: 0, max: 200, position: 'left' },
{ type: 'value', name: '完成率', min: 0, max: 100, position: 'right', axisLabel: { formatter: '{value}%' } },
2025-06-27 18:03:13 +08:00
],
series: [
{ name: '计划任务数', type: 'bar', data: countAchievedData.total || [] },
{ name: '已完成数', type: 'bar', data: countAchievedData.finish || [] },
{ name: '完成率', type: 'line', yAxisIndex: 1, data: countAchievedData.rate || [] },
2025-06-27 18:03:13 +08:00
],
2025-06-26 18:02:43 +08:00
});
}
async function fetchMaintenanceQualityScoresPei() {
const countByCusScoreData: any = await countByCusScore();
const countByCusScoreDataList = countByCusScoreData.map((item: { score: string; count: number }) => ({
value: item.count,
name: item.score,
}));
2025-06-26 18:02:43 +08:00
renderMaintenanceQualityScoresPei({
2025-06-27 18:03:13 +08:00
title: { text: '养护质量评分分布', left: 'center' },
tooltip: { trigger: 'item', formatter: '{b} : {d}%' },
2025-06-27 18:03:13 +08:00
legend: {
orient: 'horizontal',
left: 'center',
bottom: 10,
2025-07-10 17:52:54 +08:00
data: ['一星', '二星', '三星', '四星', '五星'],
2025-06-27 18:03:13 +08:00
},
series: [
{
name: '评分',
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
data: countByCusScoreDataList || [],
label: { formatter: '{b} {d}%', show: true },
2025-06-27 18:03:13 +08:00
},
],
});
}
onMounted(async () => {
loading.value = true;
try {
await fetchOrderAndAmount();
await fetchCustomers();
await fetchAchievedRate();
await fetchOrderTrend();
await fetchLeasePie();
await fetchCustomerTypesBar();
await fetchCustomerRenewalLine();
await fetchConservationTasksBar();
await fetchMaintenanceQualityScoresPei();
} finally {
loading.value = false;
}
2025-06-26 18:02:43 +08:00
});
2025-07-09 18:18:48 +08:00
// 切换视图模式
async function handleViewModeChange(e: RadioChangeEvent): Promise<void> {
2025-07-05 09:11:29 +08:00
timeUnit.value = e.target.value;
2025-07-09 18:18:48 +08:00
const res = await statisticsByTime({ timeUnit: timeUnit.value });
xAxisData.value = res?.time ?? [];
seriesData.value = res?.counts ?? [];
renderEcharts({
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: xAxisData.value,
boundaryGap: false,
},
yAxis: { type: 'value' },
series: [
{
name: '订单数',
type: 'line',
data: seriesData.value ||[],
smooth: true,
},
],
});
2025-07-05 09:11:29 +08:00
}
function formatNumber(num: number | string) {
if (!num && num !== 0) {
return '';
}
num = num.toString();
const parts = num.split('.');
let integerPart: string = parts[0] || '0';
const decimalPart = parts.length > 1 ? '.' + parts[1] : '';
const rgx = /(\d+)(\d{3})/;//整体表示匹配一组由多个数字后跟三个数字组成的字符串
while (rgx.test(integerPart)) {
integerPart = integerPart.replace(rgx, `$1${','}$2`);
}
return integerPart + decimalPart;
}
2025-06-26 18:02:43 +08:00
</script>
2025-06-25 11:17:58 +08:00
<template>
2025-06-26 18:02:43 +08:00
<div class="main">
<Spin :spinning="loading" size="large">
<div class="box">
<div class="title">
<div class="title-text">绿植租赁业务统计报表</div>
<div class="title-operate">
<div class="export" style="display: none;">
<Button size="large" style="color: #fff; background-color: #22c55e">
导出数据
</Button>
</div>
2025-06-27 18:03:13 +08:00
</div>
</div>
<div class="content">
<div class="row">
<div class="box">
<div class="title">总订单数</div>
<div class="number">{{formatNumber(countOrderAndAmountDataAmount)}}</div>
<!-- <div class="percent">8.9%</div> -->
</div>
<div class="box">
<div class="title">累计租赁金额</div>
<div class="number">{{ formatNumber(countOrderAndAmountDataOrder) }}</div>
</div>
<div class="box">
<div class="title">当前活跃客户数</div>
<div class="number">{{ formatNumber(countCustomersData) }}</div>
</div>
<div class="box">
<div class="title">绿植养护完成率</div>
<div class="number">{{ countAchievedRateData || '0.00%' }}</div>
</div>
2025-06-30 17:42:56 +08:00
</div>
<div class="row-first">
<div class="item1">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 18px; font-weight: bold;">订单数量趋势</span>
<div>
<Radio.Group v-model:value="timeUnit" @change="handleViewModeChange">
<Radio.Button value=1></Radio.Button>
<Radio.Button value=2></Radio.Button>
<Radio.Button value=3></Radio.Button>
</Radio.Group>
</div>
2025-07-05 09:11:29 +08:00
</div>
<EchartsUI
ref="orderLineRef"
height="350px"
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
<div class="item2">
<EchartsUI
ref="leasePieRef"
height="350px"
width="100%"
style="background: #fff; border-radius: 8px"
/>
2025-07-05 09:11:29 +08:00
</div>
2025-06-27 18:03:13 +08:00
</div>
<div class="row-second">
<div class="item1">
<EchartsUI
ref="customerTypesBarRef"
height="350px"
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
<div class="item2">
<EchartsUI
ref="customerRenewalLineRef"
height="350px"
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
2025-06-27 18:03:13 +08:00
</div>
<div class="row-third">
2025-06-27 18:03:13 +08:00
<EchartsUI
ref="conservationTasksBarRef"
height="100%"
2025-06-27 18:03:13 +08:00
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
<div class="row-fouth">
2025-06-27 18:03:13 +08:00
<EchartsUI
ref="maintenanceQualityScoresPeiRef"
height="100%"
2025-06-27 18:03:13 +08:00
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
</div>
</div>
</Spin>
2025-06-26 18:02:43 +08:00
</div>
</template>
<style lang="scss" scoped>
.main {
width: 100%;
2025-06-27 18:03:13 +08:00
.box {
2025-06-26 18:02:43 +08:00
height: 100%;
2025-06-27 18:03:13 +08:00
margin: 40px;
.title {
display: flex;
justify-content: space-between;
.title-text {
font-size: 25px;
font-weight: bold;
}
.title-operate {
2025-06-26 18:02:43 +08:00
display: flex;
2025-06-27 18:03:13 +08:00
.export {
margin-left: 20px;
2025-06-26 18:02:43 +08:00
}
2025-06-27 18:03:13 +08:00
}
2025-06-26 18:02:43 +08:00
}
2025-06-27 18:03:13 +08:00
.content {
flex: 1;
height: 100%;
padding: 10px;
.row {
display: flex;
justify-content: space-between;
2025-06-30 17:42:56 +08:00
.box{
2025-07-09 18:18:48 +08:00
width: 250px;
max-width: 300px;
2025-06-30 17:42:56 +08:00
height: 120px;
background-color: #fff;
border-radius: 8px;
margin: 40px 0px;
padding: 10px;
.title{
font-size: 20px;
}
.number{
font-size: 25px;
font-weight: bold;
}
.percent{
font-size: 15px; }
}
2025-06-27 18:03:13 +08:00
}
.row-first {
display: flex;
justify-content: space-between;
height: 400px;
2025-07-05 09:11:29 +08:00
margin-bottom: 50px;
2025-06-27 18:03:13 +08:00
}
.row-second {
display: flex;
justify-content: space-between;
height: 400px;
2025-07-05 09:11:29 +08:00
margin-bottom: 50px;
2025-06-27 18:03:13 +08:00
}
.row-third {
height: 400px;
margin-bottom: 50px;
2025-06-27 18:03:13 +08:00
}
.row-fouth {
height: 400px;
}
.item1 {
width: 45%;
2025-06-26 18:02:43 +08:00
height: 100%;
2025-07-05 09:11:29 +08:00
background-color: #fff;
padding: 10px;
border-radius: 8px;
// margin: 20px;
2025-06-27 18:03:13 +08:00
}
.item2 {
width: 50%;
height: 100%;
2025-07-05 09:11:29 +08:00
background-color: #fff;
padding: 10px;
border-radius: 8px;
2025-06-27 18:03:13 +08:00
}
2025-06-26 18:02:43 +08:00
}
}
}
2025-06-27 18:03:13 +08:00
</style>