|
|
@@ -6,8 +6,14 @@
|
|
|
<el-card shadow="hover">
|
|
|
<div class="flex items-center gap-10px">
|
|
|
<div class="flex items-center gap-6px">
|
|
|
- <span class="font-bold text-24px">实时数据</span>
|
|
|
- <el-icon color="#4080ff" size="20" class="cursor-pointer" @click="refreshData">
|
|
|
+ <span class="font-bold text-18px">实时数据</span>
|
|
|
+ <el-icon
|
|
|
+ color="#4080ff"
|
|
|
+ size="20"
|
|
|
+ class="cursor-pointer"
|
|
|
+ :class="isLoading ? 'is-loading' : ''"
|
|
|
+ @click="refreshData"
|
|
|
+ >
|
|
|
<Refresh />
|
|
|
</el-icon>
|
|
|
</div>
|
|
|
@@ -31,8 +37,14 @@
|
|
|
<el-card shadow="hover">
|
|
|
<div class="flex items-center gap-10px">
|
|
|
<div class="flex items-center gap-6px">
|
|
|
- <span class="font-bold text-24px">今日实时数据</span>
|
|
|
- <el-icon color="#4080ff" size="20" class="cursor-pointer" @click="refreshData">
|
|
|
+ <span class="font-bold text-18px">今日实时数据</span>
|
|
|
+ <el-icon
|
|
|
+ color="#4080ff"
|
|
|
+ size="20"
|
|
|
+ class="cursor-pointer"
|
|
|
+ :class="isLoading ? 'is-loading' : ''"
|
|
|
+ @click="refreshData"
|
|
|
+ >
|
|
|
<Refresh />
|
|
|
</el-icon>
|
|
|
</div>
|
|
|
@@ -53,7 +65,7 @@
|
|
|
<div class="h-16px"></div>
|
|
|
|
|
|
<!-- 所有站点充电度数 日时段对比趋势 -->
|
|
|
- <el-card shadow="hover">
|
|
|
+ <el-card v-loading="hourlyTrendLoading" shadow="hover">
|
|
|
<div class="flex items-center gap-16px mb-16px">
|
|
|
<span class="font-bold text-18px">所有站点充电度数 日时段对比趋势</span>
|
|
|
</div>
|
|
|
@@ -83,7 +95,7 @@
|
|
|
<div class="h-16px"></div>
|
|
|
|
|
|
<!-- 历史营业数据(面积图) -->
|
|
|
- <el-card shadow="hover">
|
|
|
+ <el-card v-loading="historyLoading" shadow="hover">
|
|
|
<div class="flex items-center justify-between mb-16px">
|
|
|
<span class="font-bold text-18px">历史营业数据</span>
|
|
|
</div>
|
|
|
@@ -122,7 +134,7 @@
|
|
|
<div class="h-16px"></div>
|
|
|
|
|
|
<!-- 历史营业数据(表格) -->
|
|
|
- <el-card shadow="hover">
|
|
|
+ <el-card v-loading="tableLoading" shadow="hover">
|
|
|
<div class="flex items-center justify-between flex-wrap gap-16px mb-16px">
|
|
|
<div class="flex items-center gap-8px">
|
|
|
<el-radio-group v-model="tableDataType" size="small">
|
|
|
@@ -131,7 +143,7 @@
|
|
|
</el-radio-group>
|
|
|
</div>
|
|
|
<div class="flex items-center gap-8px flex-wrap">
|
|
|
- <el-radio-group v-model="tableDateRange" size="small">
|
|
|
+ <el-radio-group v-model="tableDateRange" size="small" @change="handleDateRangeChange">
|
|
|
<el-radio-button value="yesterday">昨天</el-radio-button>
|
|
|
<el-radio-button value="week">近七天</el-radio-button>
|
|
|
<el-radio-button value="month">近30天</el-radio-button>
|
|
|
@@ -144,6 +156,7 @@
|
|
|
value-format="YYYY-MM-DD"
|
|
|
size="small"
|
|
|
style="width: 130px"
|
|
|
+ @change="handleStartDateChange"
|
|
|
/>
|
|
|
<span class="text-#999">至</span>
|
|
|
<el-date-picker
|
|
|
@@ -154,7 +167,9 @@
|
|
|
value-format="YYYY-MM-DD"
|
|
|
size="small"
|
|
|
style="width: 130px"
|
|
|
+ @change="handleEndDateChange"
|
|
|
/>
|
|
|
+ <el-button type="primary" size="small" @click="handleQuery">查询</el-button>
|
|
|
<span class="text-14px text-#999 ml-16px">已选时间: {{ selectedDateRange }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -163,55 +178,55 @@
|
|
|
<!-- 热门充电站 -->
|
|
|
<div class="flex-1 min-w-400px">
|
|
|
<div class="font-bold text-16px mb-12px">热门充电站</div>
|
|
|
- <el-table :data="hotStationData" border stripe size="small">
|
|
|
+ <el-table
|
|
|
+ :data="hotStationData"
|
|
|
+ stripe
|
|
|
+ size="small"
|
|
|
+ class="custom-table"
|
|
|
+ :header-cell-style="{ background: '#F3F9FF', color: '#333' }"
|
|
|
+ :row-style="rowStyle"
|
|
|
+ >
|
|
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
- <el-table-column prop="name" label="电站名称" min-width="180" />
|
|
|
+ <el-table-column prop="stationName" label="电站名称" min-width="180" />
|
|
|
<el-table-column
|
|
|
- prop="value"
|
|
|
+ prop="chargePower"
|
|
|
:label="tableDataType === 'degree' ? '充电度数' : '有效订单'"
|
|
|
width="120"
|
|
|
align="right"
|
|
|
/>
|
|
|
</el-table>
|
|
|
<div class="flex items-center justify-between mt-12px">
|
|
|
- <span class="text-12px text-#999">共{{ hotStationTotal }}条</span>
|
|
|
- <el-pagination
|
|
|
- v-model:current-page="hotCurrentPage"
|
|
|
- v-model:page-size="hotPageSize"
|
|
|
- :page-sizes="[5, 10, 20]"
|
|
|
- :total="hotStationTotal"
|
|
|
- size="small"
|
|
|
- layout="sizes, prev, pager, next, jumper"
|
|
|
- />
|
|
|
+ <span class="text-12px text-#999">共{{ hotStationData.length }}条</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 波动充电站 -->
|
|
|
<div class="flex-1 min-w-400px">
|
|
|
<div class="font-bold text-16px mb-12px">波动充电站</div>
|
|
|
- <el-table :data="fluctuationStationData" border stripe size="small">
|
|
|
+ <el-table
|
|
|
+ :data="fluctuationStationData"
|
|
|
+ stripe
|
|
|
+ size="small"
|
|
|
+ class="custom-table"
|
|
|
+ :header-cell-style="{ background: '#F3F9FF', color: '#333' }"
|
|
|
+ :row-style="rowStyle"
|
|
|
+ >
|
|
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
- <el-table-column prop="name" label="电站名称" min-width="180" />
|
|
|
- <el-table-column prop="rate" label="涨幅 | 跌幅" width="120" align="right">
|
|
|
+ <el-table-column prop="stationName" label="电站名称" min-width="180" />
|
|
|
+ <el-table-column prop="fluctuation" label="涨幅 | 跌幅" width="120" align="right">
|
|
|
<template #default="{ row }">
|
|
|
- <span :class="row.rate >= 0 ? 'text-#F56C6C' : 'text-#67C23A'">
|
|
|
- <el-icon v-if="row.rate >= 0"><CaretTop /></el-icon>
|
|
|
+ <span
|
|
|
+ :class="row.fluctuationDirection === 'up' ? 'text-#F56C6C' : 'text-#67C23A'"
|
|
|
+ >
|
|
|
+ <el-icon v-if="row.fluctuationDirection === 'up'"><CaretTop /></el-icon>
|
|
|
<el-icon v-else><CaretBottom /></el-icon>
|
|
|
- {{ Math.abs(row.rate).toFixed(2) }}%
|
|
|
+ {{ Math.abs(row.fluctuation ?? 0).toFixed(2) }}%
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
<div class="flex items-center justify-between mt-12px">
|
|
|
- <span class="text-12px text-#999">共{{ fluctuationStationTotal }}条</span>
|
|
|
- <el-pagination
|
|
|
- v-model:current-page="fluctuationCurrentPage"
|
|
|
- v-model:page-size="fluctuationPageSize"
|
|
|
- :page-sizes="[5, 10, 20]"
|
|
|
- :total="fluctuationStationTotal"
|
|
|
- size="small"
|
|
|
- layout="sizes, prev, pager, next, jumper"
|
|
|
- />
|
|
|
+ <span class="text-12px text-#999">共{{ fluctuationStationData.length }}条</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -226,9 +241,14 @@
|
|
|
<div class="flex items-center gap-200px bg-#F3F9FF rounded-8px p-32px">
|
|
|
<div>
|
|
|
<div class="text-14px text-#666 mb-8px">近七天 流失率</div>
|
|
|
- <div class="font-bold text-32px text-#333">{{ lossRate.sevenDay }}%</div>
|
|
|
+ <div class="font-bold text-32px text-#333">{{ lossRate?.sevenDayChurnRate }}%</div>
|
|
|
</div>
|
|
|
- <el-button type="primary" link @click="downloadReport('sevenDay')">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :loading="downloadLoading === 'sevenDay'"
|
|
|
+ @click="downloadReport('sevenDay')"
|
|
|
+ >
|
|
|
<el-icon><Download /></el-icon>
|
|
|
下载表格
|
|
|
</el-button>
|
|
|
@@ -236,9 +256,14 @@
|
|
|
<div class="flex items-center gap-200px bg-#F3F9FF rounded-8px p-32px">
|
|
|
<div>
|
|
|
<div class="text-14px text-#666 mb-8px">近1个月 流失率</div>
|
|
|
- <div class="font-bold text-32px text-#333">{{ lossRate.oneMonth }}%</div>
|
|
|
+ <div class="font-bold text-32px text-#333">{{ lossRate?.oneMonthChurnRate }}%</div>
|
|
|
</div>
|
|
|
- <el-button type="primary" link @click="downloadReport('oneMonth')">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :loading="downloadLoading === 'oneMonth'"
|
|
|
+ @click="downloadReport('oneMonth')"
|
|
|
+ >
|
|
|
<el-icon><Download /></el-icon>
|
|
|
下载表格
|
|
|
</el-button>
|
|
|
@@ -246,9 +271,16 @@
|
|
|
<div class="flex items-center gap-200px bg-#F3F9FF rounded-8px p-32px">
|
|
|
<div>
|
|
|
<div class="text-14px text-#666 mb-8px">近3个月 流失率</div>
|
|
|
- <div class="font-bold text-32px text-#333">{{ lossRate.threeMonth }}%</div>
|
|
|
+ <div class="font-bold text-32px text-#333">
|
|
|
+ {{ lossRate?.threeMonthChurnRate }}%
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <el-button type="primary" link @click="downloadReport('threeMonth')">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :loading="downloadLoading === 'threeMonth'"
|
|
|
+ @click="downloadReport('threeMonth')"
|
|
|
+ >
|
|
|
<el-icon><Download /></el-icon>
|
|
|
下载表格
|
|
|
</el-button>
|
|
|
@@ -263,14 +295,33 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted } from "vue";
|
|
|
+import { ref, computed, onMounted, watch } from "vue";
|
|
|
+import dataBoardApi from "@/api/operationsManage/data-kanban-api";
|
|
|
+import type {
|
|
|
+ DataBoardRealTimeVO,
|
|
|
+ DataBoardTodayVO,
|
|
|
+ ChargePowerTrendVO,
|
|
|
+ HistoryBusinessDataVO,
|
|
|
+ StationRankListVO,
|
|
|
+} from "@/api/operationsManage/data-kanban-api";
|
|
|
import { dayjs, ElMessage } from "element-plus";
|
|
|
import { CaretTop, CaretBottom, Download, Refresh } from "@element-plus/icons-vue";
|
|
|
import type { TabsPaneContext } from "element-plus";
|
|
|
import DataCard from "./components/DataCard.vue";
|
|
|
-
|
|
|
+onMounted(() => {
|
|
|
+ getTotalDataList();
|
|
|
+ getTodayDataList();
|
|
|
+ getChargePowerTrendData();
|
|
|
+ getHistoryBusinessData();
|
|
|
+ getStationRankData();
|
|
|
+ getUserLossRate();
|
|
|
+});
|
|
|
const activeName = ref("first");
|
|
|
-
|
|
|
+const rowStyle = ({ row, rowIndex }) => {
|
|
|
+ return {
|
|
|
+ background: rowIndex % 2 === 0 ? "rgba(24,144,255,0.2)" : "rgba(24,144,255,0.05)",
|
|
|
+ };
|
|
|
+};
|
|
|
// 最后更新时间
|
|
|
const lastUpdateTime = ref(dayjs().format("MM-DD HH:mm"));
|
|
|
|
|
|
@@ -279,6 +330,29 @@ const selectedCompareDate = ref(dayjs().subtract(1, "day").format("YYYY-MM-DD"))
|
|
|
// 对比日期显示格式
|
|
|
const compareDate = computed(() => dayjs(selectedCompareDate.value).format("MM.DD"));
|
|
|
|
|
|
+// 充电度数趋势数据
|
|
|
+const chargePowerTrendData = ref<ChargePowerTrendVO>({});
|
|
|
+// 日时段对比趋势图表加载状态
|
|
|
+const hourlyTrendLoading = ref(false);
|
|
|
+
|
|
|
+// 获取充电度数趋势数据
|
|
|
+const getChargePowerTrendData = () => {
|
|
|
+ hourlyTrendLoading.value = true;
|
|
|
+ dataBoardApi
|
|
|
+ .getChargePowerTrend({ compareDate: selectedCompareDate.value })
|
|
|
+ .then((res) => {
|
|
|
+ chargePowerTrendData.value = res;
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ hourlyTrendLoading.value = false;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 监听对比日期变化,重新获取数据
|
|
|
+watch(selectedCompareDate, () => {
|
|
|
+ getChargePowerTrendData();
|
|
|
+});
|
|
|
+
|
|
|
// 禁用日期:只允许选择昨天之前的15天(含昨天)
|
|
|
const disableCompareDate = (date: Date) => {
|
|
|
const today = dayjs().startOf("day");
|
|
|
@@ -290,56 +364,86 @@ const disableCompareDate = (date: Date) => {
|
|
|
};
|
|
|
|
|
|
// 刷新数据
|
|
|
+const isLoading = ref(true);
|
|
|
const refreshData = () => {
|
|
|
+ isLoading.value = true;
|
|
|
+ getTotalDataList();
|
|
|
+ getTodayDataList();
|
|
|
lastUpdateTime.value = dayjs().format("MM-DD HH:mm");
|
|
|
- ElMessage.success("数据已刷新");
|
|
|
};
|
|
|
|
|
|
// 数据指标配置
|
|
|
const dataMetrics = [
|
|
|
- { type: "cdds", label: "充电度数", icon: "cdds.png", unit: "度" },
|
|
|
- { type: "cdje", label: "充电金额", icon: "cdje.png", unit: "元" },
|
|
|
- { type: "dkqje", label: "抵扣券金额", icon: "dkqje.png", unit: "元" },
|
|
|
- { type: "fwfje", label: "服务费金额", icon: "fwfje.png", unit: "元" },
|
|
|
- { type: "tkje", label: "退款金额", icon: "tkje.png", unit: "元" },
|
|
|
- { type: "sfje", label: "实付金额", icon: "sfje.png", unit: "元" },
|
|
|
- { type: "sdje", label: "首单金额", icon: "sdje.png", unit: "元" },
|
|
|
- { type: "yhqjm", label: "优惠券减免", icon: "yhqjm.png", unit: "元" },
|
|
|
- { type: "qyzxjj", label: "企业专享价减", icon: "qyzxjj.png", unit: "元" },
|
|
|
- { type: "fxyj", label: "分销佣金", icon: "fxyj.png", unit: "元" },
|
|
|
+ { type: "cdds", label: "充电度数", icon: "cdds.png", unit: "度", key: "ChargePower" },
|
|
|
+ { type: "cdje", label: "充电金额", icon: "cdje.png", unit: "元", key: "ChargeAmount" },
|
|
|
+ { type: "dkqje", label: "抵扣券金额", icon: "dkqje.png", unit: "元", key: "DiscountAmount" },
|
|
|
+ { type: "fwfje", label: "服务费金额", icon: "fwfje.png", unit: "元", key: "ServiceFeeAmount" },
|
|
|
+ { type: "tkje", label: "退款金额", icon: "tkje.png", unit: "元", key: "RefundAmount" },
|
|
|
+ { type: "sfje", label: "实付金额", icon: "sfje.png", unit: "元", key: "ActualPayAmount" },
|
|
|
+ { type: "sdje", label: "首单金额", icon: "sdje.png", unit: "元", key: "FirstOrderAmount" },
|
|
|
+ { type: "yhqjm", label: "优惠券减免", icon: "yhqjm.png", unit: "元", key: "CouponAmount" },
|
|
|
+ {
|
|
|
+ type: "qyzxjj",
|
|
|
+ label: "企业专享价减",
|
|
|
+ icon: "qyzxjj.png",
|
|
|
+ unit: "元",
|
|
|
+ key: "FirmDiscountAmount",
|
|
|
+ },
|
|
|
+ { type: "fxyj", label: "分销佣金", icon: "fxyj.png", unit: "元", key: "CommissionAmount" },
|
|
|
];
|
|
|
|
|
|
+// 总数据
|
|
|
+const totalData = ref<DataBoardRealTimeVO>({});
|
|
|
+
|
|
|
// 总数据列表
|
|
|
const totalDataList = computed(() =>
|
|
|
dataMetrics.map((item) => ({
|
|
|
...item,
|
|
|
label: `总${item.label}`,
|
|
|
- value: "198,708.45",
|
|
|
+ value: formatNumber(totalData.value[`total${item.key}` as keyof DataBoardRealTimeVO]),
|
|
|
}))
|
|
|
);
|
|
|
|
|
|
+const getTotalDataList = () => {
|
|
|
+ dataBoardApi.getRealTimeData().then((res) => {
|
|
|
+ isLoading.value = false;
|
|
|
+ ElMessage.success("数据已刷新");
|
|
|
+ totalData.value = res;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 今日数据
|
|
|
+const todayData = ref<DataBoardTodayVO>({});
|
|
|
+
|
|
|
// 今日数据列表
|
|
|
const todayDataList = computed(() =>
|
|
|
dataMetrics.map((item) => ({
|
|
|
...item,
|
|
|
label: `今日${item.label}`,
|
|
|
- value: "198,708.45",
|
|
|
+ value: formatNumber(todayData.value[`today${item.key}` as keyof DataBoardTodayVO]),
|
|
|
}))
|
|
|
);
|
|
|
|
|
|
+const getTodayDataList = () => {
|
|
|
+ dataBoardApi.getTodayRealTimeData().then((res) => {
|
|
|
+ isLoading.value = false;
|
|
|
+ todayData.value = res;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化数字
|
|
|
+const formatNumber = (value: number | undefined): string => {
|
|
|
+ if (value === undefined || value === null) return "0.00";
|
|
|
+ return value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
|
+};
|
|
|
+
|
|
|
// ==================== 日时段对比趋势图表 ====================
|
|
|
const hourlyTrendChartOptions = computed(() => {
|
|
|
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
|
- // 模拟今日数据
|
|
|
- const todayData = [
|
|
|
- 50, 80, 120, 150, 200, 280, 350, 450, 520, 580, 620, 580, 520, 480, 450, 420, 400, 450, 500,
|
|
|
- 480, 420, 350, 200, 100,
|
|
|
- ];
|
|
|
- // 模拟昨日数据
|
|
|
- const yesterdayData = [
|
|
|
- 40, 70, 100, 130, 180, 250, 320, 420, 480, 550, 600, 560, 500, 460, 430, 400, 380, 420, 470,
|
|
|
- 450, 400, 320, 180, 80,
|
|
|
- ];
|
|
|
+ // 今日数据
|
|
|
+ const todayData = chargePowerTrendData.value?.todayData ?? [];
|
|
|
+ // 对比日数据
|
|
|
+ const compareData = chargePowerTrendData.value?.compareData ?? [];
|
|
|
|
|
|
return {
|
|
|
tooltip: {
|
|
|
@@ -409,7 +513,7 @@ const hourlyTrendChartOptions = computed(() => {
|
|
|
{
|
|
|
name: compareDate.value,
|
|
|
type: "line",
|
|
|
- data: yesterdayData,
|
|
|
+ data: compareData,
|
|
|
smooth: true,
|
|
|
symbol: "circle",
|
|
|
symbolSize: 6,
|
|
|
@@ -443,38 +547,67 @@ const historyTimeType = ref("day");
|
|
|
const historyDateValue = ref(dayjs().format("YYYY-MM"));
|
|
|
const historyMetric = ref("chargeDegree");
|
|
|
|
|
|
-// 监听时间维度变化,重置日期值
|
|
|
+// 历史营业数据
|
|
|
+const historyBusinessData = ref<HistoryBusinessDataVO>({});
|
|
|
+// 历史营业数据图表加载状态
|
|
|
+const historyLoading = ref(false);
|
|
|
+
|
|
|
+// 指标映射:前端值 -> API值
|
|
|
+const metricToApiMap: Record<string, string> = {
|
|
|
+ chargeDegree: "chargePower",
|
|
|
+ chargeAmount: "chargeAmount",
|
|
|
+ validOrder: "validOrders",
|
|
|
+ registerUser: "registerUsers",
|
|
|
+};
|
|
|
+
|
|
|
+// 指标标签映射
|
|
|
+const metricLabels: Record<string, string> = {
|
|
|
+ chargeDegree: "充电度数",
|
|
|
+ chargeAmount: "充电金额",
|
|
|
+ validOrder: "有效订单",
|
|
|
+ registerUser: "注册用户",
|
|
|
+};
|
|
|
+
|
|
|
+// 获取历史营业数据
|
|
|
+const getHistoryBusinessData = () => {
|
|
|
+ const dataType = metricToApiMap[historyMetric.value];
|
|
|
+ const timeDimension = historyTimeType.value;
|
|
|
+ const yearMonth = historyDateValue.value;
|
|
|
+
|
|
|
+ historyLoading.value = true;
|
|
|
+ dataBoardApi
|
|
|
+ .getHistoryBusinessData({
|
|
|
+ dataType,
|
|
|
+ timeDimension,
|
|
|
+ yearMonth,
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ historyBusinessData.value = res;
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ historyLoading.value = false;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 监听时间维度变化,重置日期值并重新获取数据
|
|
|
watch(historyTimeType, (newType) => {
|
|
|
if (newType === "day") {
|
|
|
historyDateValue.value = dayjs().format("YYYY-MM");
|
|
|
} else {
|
|
|
historyDateValue.value = dayjs().format("YYYY");
|
|
|
}
|
|
|
+ getHistoryBusinessData();
|
|
|
});
|
|
|
|
|
|
-const historyAreaChartOptions = computed(() => {
|
|
|
- let dates: string[];
|
|
|
- let mockData: number[];
|
|
|
-
|
|
|
- if (historyTimeType.value === "day") {
|
|
|
- // 日趋势:显示月份中的每一天
|
|
|
- const daysInMonth = dayjs(historyDateValue.value).daysInMonth();
|
|
|
- dates = Array.from({ length: daysInMonth }, (_, i) =>
|
|
|
- dayjs(historyDateValue.value).startOf("month").add(i, "day").format("MM.DD")
|
|
|
- );
|
|
|
- mockData = Array.from({ length: daysInMonth }, () => Math.floor(Math.random() * 5000) + 2000);
|
|
|
- } else {
|
|
|
- // 月趋势:显示年份中的12个月
|
|
|
- dates = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
|
|
|
- mockData = Array.from({ length: 12 }, () => Math.floor(Math.random() * 50000) + 20000);
|
|
|
- }
|
|
|
+// 监听日期和指标变化,重新获取数据
|
|
|
+watch([historyDateValue, historyMetric], () => {
|
|
|
+ getHistoryBusinessData();
|
|
|
+});
|
|
|
|
|
|
- const metricLabels: Record<string, string> = {
|
|
|
- chargeDegree: "充电度数",
|
|
|
- chargeAmount: "充电金额",
|
|
|
- validOrder: "有效订单",
|
|
|
- registerUser: "注册用户",
|
|
|
- };
|
|
|
+const historyAreaChartOptions = computed(() => {
|
|
|
+ // 使用API返回的数据
|
|
|
+ const dates = historyBusinessData.value?.labels ?? [];
|
|
|
+ const dataValues = historyBusinessData.value?.values ?? [];
|
|
|
|
|
|
return {
|
|
|
tooltip: {
|
|
|
@@ -506,7 +639,7 @@ const historyAreaChartOptions = computed(() => {
|
|
|
{
|
|
|
name: metricLabels[historyMetric.value],
|
|
|
type: "line",
|
|
|
- data: mockData,
|
|
|
+ data: dataValues,
|
|
|
smooth: true,
|
|
|
areaStyle: {
|
|
|
color: {
|
|
|
@@ -539,15 +672,24 @@ const tableDateRange = ref("week");
|
|
|
const tableStartDate = ref("");
|
|
|
const tableEndDate = ref("");
|
|
|
|
|
|
-// 热门充电站分页
|
|
|
-const hotCurrentPage = ref(1);
|
|
|
-const hotPageSize = ref(5);
|
|
|
-const hotStationTotal = ref(50);
|
|
|
+// 表格加载状态
|
|
|
+const tableLoading = ref(false);
|
|
|
|
|
|
-// 波动充电站分页
|
|
|
-const fluctuationCurrentPage = ref(1);
|
|
|
-const fluctuationPageSize = ref(5);
|
|
|
-const fluctuationStationTotal = ref(50);
|
|
|
+// 站点排名数据
|
|
|
+const stationRankData = ref<StationRankListVO>({});
|
|
|
+
|
|
|
+// 统计类型映射:前端值 -> API值
|
|
|
+const statisticTypeMap: Record<string, string> = {
|
|
|
+ degree: "chargePower",
|
|
|
+ order: "validOrders",
|
|
|
+};
|
|
|
+
|
|
|
+// 时间范围映射:前端值 -> API值
|
|
|
+const timeRangeMap: Record<string, string> = {
|
|
|
+ yesterday: "today",
|
|
|
+ week: "week",
|
|
|
+ month: "month",
|
|
|
+};
|
|
|
|
|
|
// 选中的日期范围显示
|
|
|
const selectedDateRange = computed(() => {
|
|
|
@@ -568,30 +710,89 @@ const selectedDateRange = computed(() => {
|
|
|
});
|
|
|
|
|
|
// 热门充电站数据
|
|
|
-const hotStationData = computed(() => [
|
|
|
- { name: "小关停车场B区-小站", value: 100.01 },
|
|
|
- { name: "延安西路智慧停车楼", value: 99.01 },
|
|
|
- { name: "花果园立交桥南广场停车场", value: 97.01 },
|
|
|
- { name: "花果园立交北广场停车场", value: 97 },
|
|
|
- { name: "轩宇智慧停车场", value: 96 },
|
|
|
-]);
|
|
|
+const hotStationData = computed(() => stationRankData.value?.hotStations ?? []);
|
|
|
|
|
|
// 波动充电站数据
|
|
|
-const fluctuationStationData = computed(() => [
|
|
|
- { name: "小关停车场B区-小站", rate: 6.29 },
|
|
|
- { name: "延安西路智慧停车楼", rate: 5.34 },
|
|
|
- { name: "花果园立交桥南广场停车场", rate: 4.21 },
|
|
|
- { name: "花果园立交北广场停车场", rate: -3.52 },
|
|
|
- { name: "轩宇智慧停车场", rate: -1.98 },
|
|
|
-]);
|
|
|
+const fluctuationStationData = computed(() => stationRankData.value?.fluctuationStations ?? []);
|
|
|
|
|
|
-// ==================== 用户流失率 ====================
|
|
|
-const lossRate = ref({
|
|
|
- sevenDay: "59.99",
|
|
|
- oneMonth: "70.12",
|
|
|
- threeMonth: "70.12",
|
|
|
+// 获取站点排名数据
|
|
|
+const getStationRankData = () => {
|
|
|
+ const statisticType = statisticTypeMap[tableDataType.value];
|
|
|
+ let timeRange = timeRangeMap[tableDateRange.value];
|
|
|
+ let startDate: string | undefined;
|
|
|
+ let endDate: string | undefined;
|
|
|
+
|
|
|
+ // 自定义时间范围
|
|
|
+ if (tableStartDate.value && tableEndDate.value) {
|
|
|
+ timeRange = "custom";
|
|
|
+ startDate = tableStartDate.value;
|
|
|
+ endDate = tableEndDate.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ tableLoading.value = true;
|
|
|
+ dataBoardApi
|
|
|
+ .getStationRankData({
|
|
|
+ statisticType,
|
|
|
+ timeRange,
|
|
|
+ startDate,
|
|
|
+ endDate,
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ stationRankData.value = res;
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ tableLoading.value = false;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 监听表格数据类型变化,重新获取数据
|
|
|
+watch(tableDataType, () => {
|
|
|
+ getStationRankData();
|
|
|
});
|
|
|
|
|
|
+// 日期范围按钮变化处理
|
|
|
+const handleDateRangeChange = () => {
|
|
|
+ // 清空手动选择的日期
|
|
|
+ tableStartDate.value = "";
|
|
|
+ tableEndDate.value = "";
|
|
|
+ getStationRankData();
|
|
|
+};
|
|
|
+
|
|
|
+// 开始日期变化处理
|
|
|
+const handleStartDateChange = () => {
|
|
|
+ // 清空按钮选择
|
|
|
+ tableDateRange.value = "";
|
|
|
+ // 如果结束日期已选,自动触发查询
|
|
|
+ if (tableEndDate.value) {
|
|
|
+ getStationRankData();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 结束日期变化处理
|
|
|
+const handleEndDateChange = () => {
|
|
|
+ // 清空按钮选择
|
|
|
+ tableDateRange.value = "";
|
|
|
+ // 如果开始日期已选,自动触发查询
|
|
|
+ if (tableStartDate.value) {
|
|
|
+ getStationRankData();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 查询按钮点击处理
|
|
|
+const handleQuery = () => {
|
|
|
+ getStationRankData();
|
|
|
+};
|
|
|
+
|
|
|
+// ==================== 用户流失率 ====================
|
|
|
+const lossRate = ref();
|
|
|
+const getUserLossRate = () => {
|
|
|
+ dataBoardApi.getUserChurnRate().then((res) => {
|
|
|
+ lossRate.value = res;
|
|
|
+ });
|
|
|
+};
|
|
|
+// 下载报表loading状态
|
|
|
+const downloadLoading = ref<string>("");
|
|
|
+
|
|
|
// 下载报表
|
|
|
const downloadReport = (type: string) => {
|
|
|
const typeLabels: Record<string, string> = {
|
|
|
@@ -599,16 +800,43 @@ const downloadReport = (type: string) => {
|
|
|
oneMonth: "近1个月",
|
|
|
threeMonth: "近3个月",
|
|
|
};
|
|
|
- ElMessage.success(`正在下载${typeLabels[type]}流失率报表...`);
|
|
|
+
|
|
|
+ // 类型映射
|
|
|
+ const periodTypeMap: Record<string, string> = {
|
|
|
+ sevenDay: "7d",
|
|
|
+ oneMonth: "1m",
|
|
|
+ threeMonth: "3m",
|
|
|
+ };
|
|
|
+
|
|
|
+ downloadLoading.value = type;
|
|
|
+ dataBoardApi
|
|
|
+ .exportUserChurnRate({ periodType: periodTypeMap[type] })
|
|
|
+ .then((res: any) => {
|
|
|
+ // 创建Blob对象(res 是 axios response 对象,需要使用 res.data 获取 Blob 数据)
|
|
|
+ const blob = new Blob([res.data], {
|
|
|
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
+ });
|
|
|
+ // 创建下载链接
|
|
|
+ const link = document.createElement("a");
|
|
|
+ link.href = URL.createObjectURL(blob);
|
|
|
+ link.download = `用户流失率统计_${typeLabels[type]}_${dayjs().format("YYYY-MM-DD")}.xlsx`;
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ URL.revokeObjectURL(link.href);
|
|
|
+ ElMessage.success(`${typeLabels[type]}流失率报表下载成功`);
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ ElMessage.error("下载失败,请重试");
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ downloadLoading.value = "";
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
|
|
console.log(tab, event);
|
|
|
};
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- // 初始化数据
|
|
|
-});
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -617,4 +845,7 @@ onMounted(() => {
|
|
|
padding: 0;
|
|
|
}
|
|
|
}
|
|
|
+.custom-table {
|
|
|
+ background-color: #f3f9ff;
|
|
|
+}
|
|
|
</style>
|