498 lines
14 KiB
Vue
498 lines
14 KiB
Vue
<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>
|