| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 |
- <script setup lang="ts">
- import { markRaw, nextTick, onActivated, onMounted, onBeforeUnmount, reactive, ref, watch } from "vue";
- import * as echarts from "echarts/core";
- import { PieChart, BarChart, LineChart } from "echarts/charts";
- import {
- GridComponent,
- TitleComponent,
- LegendComponent,
- TooltipComponent
- } from "echarts/components";
- import { CanvasRenderer } from "echarts/renderers";
- echarts.use([
- PieChart, BarChart, LineChart,
- GridComponent, TitleComponent, LegendComponent, TooltipComponent,
- CanvasRenderer
- ]);
- import { getDashboard, getTrend, getWashDeviceStatus } from "@/api/stat";
- import { emitter } from "@/utils/mitt";
- defineOptions({ name: "Dashboard" });
- const homeLineRef = ref();
- const homePieRef = ref();
- const end = new Date();
- const start = new Date();
- start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
- const fmtMoney = (value: number) => {
- if (!value) return "¥0.00";
- return `¥${(value / 100).toFixed(2)}`;
- };
- const fmtNumber = (value: any) => {
- if (value === null || value === undefined) return "0";
- return String(value);
- };
- const formatDate = (date: Date) => {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- return `${year}-${month}-${day}`;
- };
- const dateDiff = (start: Date, end: Date) => {
- return Math.ceil(Math.abs(end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
- };
- const Session = {
- get: (key: string) => {
- const value = sessionStorage.getItem(key);
- return value ? JSON.parse(value) : null;
- }
- };
- const state = reactive({
- currentStationId: null as string | null,
- dateRange: [formatDate(start), formatDate(end)] as [string, string],
- global: {
- homeChartOne: null as any,
- homeChartTwo: null as any
- },
- metrics: [
- { key: "registeredMembers", value: "0", label: "今日注册会员数", unit: "人", color: "#5470C6" },
- { key: "todayIncome", value: "0", label: "今日收益金额", unit: "元", color: "#91CC75" },
- { key: "consumptionAmount", value: "0", label: "今日消费总额", unit: "元", color: "#FAC858" },
- { key: "avgOrderPrice", value: "0", label: "订单平均消费", unit: "元", color: "#EE6666" },
- { key: "todayOrders", value: "0", label: "今日订单数量", unit: "笔", color: "#73C0DE" },
- { key: "avgDuration", value: "0", label: "洗车平均时长", unit: "分钟", color: "#8B7EC8" }
- ] as { key: string; value: string; label: string; unit: string; color: string }[],
- myCharts: [] as any[],
- charts: {
- theme: "",
- bgColor: "",
- color: "#303133"
- },
- homeOneExtra: {
- totalIncome: 0,
- totalWashOrders: 0
- }
- });
- const shortcuts = [
- {
- text: "近7天",
- value: () => {
- const end = new Date();
- const start = new Date();
- start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
- return [start, end];
- }
- },
- {
- text: "近30天",
- value: () => {
- const end = new Date();
- const start = new Date();
- start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
- return [start, end];
- }
- },
- {
- text: "近90天",
- value: () => {
- const end = new Date();
- const start = new Date();
- start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
- return [start, end];
- }
- }
- ];
- const initLineChart = (dataList: Array<any>) => {
- if (!state.myCharts.includes(state.global.homeChartOne)) {
- state.global.homeChartOne?.dispose();
- }
- state.global.homeChartOne = markRaw(echarts.init(homeLineRef.value, state.charts.theme));
- dataList.forEach(item => {
- item.startTime = item.statTime.slice(0, 3).join("-");
- item.seq = Number(item.statTime.join(""));
- });
- state.homeOneExtra.totalIncome = dataList.reduce((k, v) => k + v.totalAmount, 0);
- state.homeOneExtra.totalWashOrders = dataList.reduce((k, v) => k + v.totalOrders, 0);
- dataList.sort((a, b) => a.seq - b.seq);
- const option = {
- backgroundColor: state.charts.bgColor,
- title: {
- text: "洗车数据走势图",
- left: 0,
- top: 0,
- textStyle: { fontSize: 15, color: state.charts.color }
- },
- grid: { top: 70, right: 80, bottom: 30, left: 60 },
- tooltip: {
- trigger: "axis",
- formatter: (params: any) => {
- const bar = params.find((p: any) => p.seriesName === "洗车量");
- const line = params.find((p: any) => p.seriesName === "总金额");
- const barVal = bar ? `${bar.value} 次` : "";
- const lineVal = line ? `¥${line.value}` : "";
- return `${params[0].axisValue}<br/>${barVal}<br/>${lineVal}`;
- }
- },
- legend: { data: ["洗车量", "总金额"], right: 10, top: 5 },
- xAxis: {
- data: dataList.map(k => k.startTime),
- axisLine: { lineStyle: { color: state.charts.color } }
- },
- yAxis: [
- {
- type: "value",
- name: "洗车量/次",
- position: "left",
- axisLabel: { color: "#68a7a0" },
- splitLine: { show: true, lineStyle: { type: "dashed", color: "#f0f0f0" } }
- },
- {
- type: "value",
- name: "费用/元",
- position: "right",
- axisLabel: { color: "#409EFF" },
- splitLine: { show: false }
- }
- ],
- series: [
- {
- name: "洗车量",
- type: "bar",
- barWidth: 10,
- yAxisIndex: 0,
- data: dataList.map(k => k.totalOrders),
- itemStyle: {
- color: "#68a7a0",
- borderColor: "#68a7a0",
- barBorderRadius: 5
- }
- },
- {
- name: "总金额",
- type: "line",
- symbolSize: 6,
- symbol: "circle",
- smooth: true,
- yAxisIndex: 1,
- data: dataList.map(k => (k.totalAmount / 100).toFixed(2)),
- lineStyle: { color: "#409EFF" },
- itemStyle: { color: "#409EFF", borderColor: "#409EFF" }
- }
- ]
- };
- state.global.homeChartOne.setOption(option);
- state.myCharts.push(state.global.homeChartOne);
- };
- const initPieChart = (dataMap: any) => {
- if (!state.myCharts.includes(state.global.homeChartTwo)) {
- state.global.homeChartTwo?.dispose();
- }
- state.global.homeChartTwo = markRaw(echarts.init(homePieRef.value, state.charts.theme));
- const sessionDicts = Session.get("dicts");
- let dicts: any[] = [];
- if (sessionDicts) {
- dicts = sessionDicts["WashDevice.state"] || [];
- }
- if (!dicts.length) return;
- const getname = dicts.map(k => k.name);
- const colorList = ["#6B6F75", "#36C78B", "#e9ee8e", "#ffa496", "#E790E8", "#363638"];
- const data: any[] = [];
- for (let i = 0; i < getname.length; i++) {
- const dict = dicts.find(k => k.name === getname[i]);
- data.push({ name: getname[i], value: dataMap[`${dict?.value}`] || 0 });
- }
- const option = {
- backgroundColor: state.charts.bgColor,
- title: {
- text: "洗车设备状态",
- left: 0,
- textStyle: { fontSize: 15, color: state.charts.color }
- },
- tooltip: { trigger: "item", formatter: "{b}<br/>{c} 台" },
- legend: {
- type: "scroll",
- orient: "vertical",
- right: 0,
- left: "62%",
- top: "center",
- itemWidth: 14,
- itemHeight: 14,
- data: getname,
- textStyle: { color: state.charts.color }
- },
- series: [
- {
- type: "pie",
- radius: ["82", "102"],
- center: ["30%", "50%"],
- itemStyle: {
- color: (params: any) => colorList[params.dataIndex % colorList.length]
- },
- label: { show: false },
- labelLine: { show: false },
- data
- }
- ]
- };
- state.global.homeChartTwo.setOption(option);
- state.myCharts.push(state.global.homeChartTwo);
- };
- const initEchartsResize = () => {
- nextTick(() => {
- for (let i = 0; i < state.myCharts.length; i++) {
- state.myCharts[i]?.resize();
- }
- });
- };
- const loadCurrentEquipmentStatus = () => {
- getWashDeviceStatus(state.currentStationId || undefined).then((res: any) => {
- initPieChart(res?.data || res || {});
- });
- };
- const loadStationStat = () => {
- const start = state.dateRange[0];
- const end = state.dateRange[1];
- const size = dateDiff(new Date(start), new Date(end)) + 1;
- getTrend({
- startTime: start,
- endTime: end,
- type: "day",
- pageSize: size,
- stationId: state.currentStationId || undefined
- }).then((res: any) => {
- initLineChart(res?.data || res);
- });
- };
- const loadStationStatToday = () => {
- getDashboard(state.currentStationId || undefined).then((res: any) => {
- const data = res?.data || res;
- if (data) {
- state.metrics[0].value = fmtNumber(data.todayRegisteredMembers);
- state.metrics[1].value = fmtMoney(data.todayIncome);
- state.metrics[2].value = fmtMoney(data.todayConsumptionAmount);
- state.metrics[3].value = fmtMoney(data.avgOrderPrice);
- state.metrics[4].value = fmtNumber(data.todayWashOrders);
- state.metrics[5].value = ((data.avgOrderDuration || 0) / 60).toFixed(1);
- }
- });
- };
- const loadAll = () => {
- loadStationStat();
- loadStationStatToday();
- loadCurrentEquipmentStatus();
- };
- onMounted(() => {
- emitter.on("stationChangeRefresh", (stationId) => {
- state.currentStationId = stationId;
- state.myCharts = [];
- nextTick(() => loadAll());
- });
- const currentStationId = Session.get("currentStationId");
- if (currentStationId) {
- state.currentStationId = currentStationId;
- loadAll();
- }
- window.addEventListener("resize", initEchartsResize);
- });
- onBeforeUnmount(() => {
- emitter.off("stationChangeRefresh");
- window.removeEventListener("resize", initEchartsResize);
- });
- onActivated(() => {
- initEchartsResize();
- });
- watch(
- () => state.charts.theme,
- () => {
- nextTick(() => {
- state.myCharts = [];
- setTimeout(() => loadStationStat(), 300);
- setTimeout(() => loadCurrentEquipmentStatus(), 500);
- });
- }
- );
- </script>
- <template>
- <div class="dashboard-container">
- <!-- 未选择站点 -->
- <el-alert
- v-if="!state.currentStationId"
- title="未选择站点"
- type="warning"
- description="请通过右上角站点选择器切换到要查看的门店"
- show-icon
- closable
- class="mb15"
- />
- <!-- 6指标卡片 -->
- <el-row :gutter="15" class="metrics-row">
- <el-col
- v-for="(m, idx) in state.metrics"
- :key="m.key"
- :xs="12" :sm="8" :md="4" :lg="4" :xl="4"
- >
- <div class="metric-card">
- <div class="metric-value" :style="{ color: m.color }">{{ m.value }}</div>
- <div class="metric-label">{{ m.label }}</div>
- </div>
- </el-col>
- </el-row>
- <!-- 走势图 + 饼图 -->
- <el-row :gutter="15">
- <el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16">
- <div class="chart-panel">
- <div class="chart-toolbar">
- <el-date-picker
- @change="loadStationStat"
- value-format="YYYY-MM-DD"
- v-model="state.dateRange"
- type="daterange"
- unlink-panels
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :shortcuts="shortcuts"
- style="width: 280px"
- />
- <div class="chart-summary">
- 总收益:
- <el-tag type="success" effect="plain" size="small">{{ fmtMoney(state.homeOneExtra.totalIncome) }}元</el-tag>
- 总订单:
- <el-tag type="danger" effect="plain" size="small">{{ state.homeOneExtra.totalWashOrders }}笔</el-tag>
- </div>
- </div>
- <div class="chart-body" ref="homeLineRef" />
- </div>
- </el-col>
- <el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8">
- <div class="chart-panel">
- <div class="chart-body" ref="homePieRef" />
- </div>
- </el-col>
- </el-row>
- </div>
- </template>
- <style scoped lang="scss">
- .dashboard-container {
- padding: 20px;
- }
- // 指标卡片
- .metrics-row {
- margin-bottom: 15px;
- }
- .metric-card {
- height: 120px;
- border-radius: 6px;
- padding: 20px;
- margin-bottom: 0;
- background: #fff;
- border: 1px solid var(--el-border-color-lighter);
- transition: box-shadow 0.3s;
- &:hover {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
- }
- .metric-value {
- font-size: 30px;
- font-weight: 700;
- line-height: 1.3;
- letter-spacing: -0.5px;
- }
- .metric-label {
- margin-top: 8px;
- font-size: 14px;
- color: var(--el-text-color-secondary);
- }
- }
- // 图表面板
- .chart-panel {
- width: 100%;
- background: #fff;
- border: 1px solid var(--el-border-color-lighter);
- border-radius: 6px;
- padding: 20px;
- margin-bottom: 15px;
- transition: box-shadow 0.3s;
- &:hover {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
- }
- .chart-toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 10px;
- margin-bottom: 10px;
- .chart-summary {
- font-size: 14px;
- color: var(--el-text-color-secondary);
- }
- }
- .chart-body {
- height: 380px;
- width: 100%;
- }
- }
- </style>
|