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

498 lines
14 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.

<script setup lang="ts">
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';
import type { RadioChangeEvent } from 'ant-design-vue';
import { statisticsByTime,
countByRentalType,
countByCusType,
countRenewRate,
countByCusScore,
countOrderAndAmount,
countCustomers,
countAchievedRate,
countAchieved
} from '#/api/property/reportStatistics';
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);
const { renderEcharts: renderCustomerTypesBar } =
useEcharts(customerTypesBarRef);
const { renderEcharts: renderCustomerRenewalLine } = useEcharts(
customerRenewalLineRef,
);
const { renderEcharts: renderConservationTasksBar } = useEcharts(
conservationTasksBarRef,
);
const { renderEcharts: renderMaintenanceQualityScoresPei } = useEcharts(
maintenanceQualityScoresPeiRef,
);
const timeUnit = ref<number>(1)
const countOrderAndAmountDataAmount = ref<number>(0);
const countOrderAndAmountDataOrder = ref<number>(0);
const countAchievedRateData = ref<any>(null);
const countCustomersData = ref<any>(0);
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() {
const countCustomersDataRes: any = await countCustomers();
countCustomersData.value = countCustomersDataRes.count;
}
async function fetchAchievedRate() {
const countAchievedRateDataRes: any = await countAchievedRate();
countAchievedRateData.value = countAchievedRateDataRes.rate;
}
async function fetchOrderTrend() {
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', axisLabel: { formatter: (value) => `${value * 100}%` } },
series: [
{
name: '订单趋势',
type: 'line',
data: seriesData.value || [],
smooth: true,
},
],
});
}
async function fetchLeasePie() {
const data = await countByRentalType();
const convertedData = data.map((item: { amount: number; type: string }) => ({
value: item.amount,
name: item.type,
}));
renderLeasePie({
title: { text: '租赁金额分布', left: 'center' },
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: 'left' },
series: [
{
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
data: convertedData || [],
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();
renderCustomerTypesBar({
title: { text: '客户类型分布' },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: ['企业客户', '个人客户', '政府机构', '商业地产', '其他'],
boundaryGap: true,
},
yAxis: { type: 'value' },
series: [
{
name: '客户数',
type: 'bar',
data: countByCusTypeData.counts || [],
},
],
});
}
async function fetchCustomerRenewalLine() {
const countRenewRateData: any = await countRenewRate();
renderCustomerRenewalLine({
title: { text: '客户续租率趋势' },
tooltip: {
trigger: 'axis',
formatter: function (params: any) {
let result = params[0].axisValue + '<br/>';
params.forEach((item: any) => {
result += item.marker + item.seriesName + '' + item.data + '%<br/>';
});
return result;
},
},
xAxis: {
type: 'category',
data: countRenewRateData.month || [],
boundaryGap: false,
},
yAxis: {
type: 'value',
axisLabel: { formatter: '{value}%' },
},
series: [
{
name: '续租率',
type: 'line',
data: countRenewRateData.rate || [],
smooth: true,
},
],
});
}
async function fetchConservationTasksBar() {
const countAchievedData: any = await countAchieved();
renderConservationTasksBar({
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;
},
},
legend: { data: ['计划任务数', '已完成数', '完成率'] },
xAxis: [
{
type: 'category',
data: ['修剪整形', '肥水管理', '中耕除草', '病虫害防治', '越冬防寒'],
},
],
yAxis: [
{ type: 'value', name: '任务数', min: 0, max: 200, position: 'left' },
{ type: 'value', name: '完成率', min: 0, max: 100, position: 'right', axisLabel: { formatter: '{value}%' } },
],
series: [
{ name: '计划任务数', type: 'bar', data: countAchievedData.total || [] },
{ name: '已完成数', type: 'bar', data: countAchievedData.finish || [] },
{ name: '完成率', type: 'line', yAxisIndex: 1, data: countAchievedData.rate || [] },
],
});
}
async function fetchMaintenanceQualityScoresPei() {
const countByCusScoreData: any = await countByCusScore();
const countByCusScoreDataList = countByCusScoreData.map((item: { score: string; count: number }) => ({
value: item.count,
name: item.score,
}));
renderMaintenanceQualityScoresPei({
title: { text: '养护质量评分分布', left: 'center' },
tooltip: { trigger: 'item', formatter: '{b} : {d}%' },
legend: {
orient: 'horizontal',
left: 'center',
bottom: 10,
data: ['一星', '二星', '三星', '四星', '五星'],
},
series: [
{
name: '评分',
type: 'pie',
radius: '60%',
center: ['50%', '50%'],
data: countByCusScoreDataList || [],
label: { formatter: '{b} {d}%', show: true },
},
],
});
}
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;
}
});
// 切换视图模式
async function handleViewModeChange(e: RadioChangeEvent): Promise<void> {
timeUnit.value = e.target.value;
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,
},
],
});
}
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;
}
</script>
<template>
<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>
</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>
</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>
</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"
/>
</div>
</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>
</div>
<div class="row-third">
<EchartsUI
ref="conservationTasksBarRef"
height="100%"
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
<div class="row-fouth">
<EchartsUI
ref="maintenanceQualityScoresPeiRef"
height="100%"
width="100%"
style="background: #fff; border-radius: 8px"
/>
</div>
</div>
</div>
</Spin>
</div>
</template>
<style lang="scss" scoped>
.main {
width: 100%;
.box {
height: 100%;
margin: 40px;
.title {
display: flex;
justify-content: space-between;
.title-text {
font-size: 25px;
font-weight: bold;
}
.title-operate {
display: flex;
.export {
margin-left: 20px;
}
}
}
.content {
flex: 1;
height: 100%;
padding: 10px;
.row {
display: flex;
justify-content: space-between;
.box{
width: 250px;
max-width: 300px;
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; }
}
}
.row-first {
display: flex;
justify-content: space-between;
height: 400px;
margin-bottom: 50px;
}
.row-second {
display: flex;
justify-content: space-between;
height: 400px;
margin-bottom: 50px;
}
.row-third {
height: 400px;
margin-bottom: 50px;
}
.row-fouth {
height: 400px;
}
.item1 {
width: 45%;
height: 100%;
background-color: #fff;
padding: 10px;
border-radius: 8px;
// margin: 20px;
}
.item2 {
width: 50%;
height: 100%;
background-color: #fff;
padding: 10px;
border-radius: 8px;
}
}
}
}
</style>