|
|
@@ -1,46 +1,39 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { markRaw, nextTick, onActivated, onMounted, reactive, ref, watch, computed } from "vue";
|
|
|
+import { markRaw, nextTick, onActivated, onMounted, reactive, ref, watch } from "vue";
|
|
|
import * as echarts from "echarts/core";
|
|
|
import { PieChart, BarChart, LineChart } from "echarts/charts";
|
|
|
import {
|
|
|
GridComponent,
|
|
|
TitleComponent,
|
|
|
LegendComponent,
|
|
|
- TooltipComponent,
|
|
|
- DataZoomComponent
|
|
|
+ TooltipComponent
|
|
|
} from "echarts/components";
|
|
|
import { CanvasRenderer } from "echarts/renderers";
|
|
|
|
|
|
echarts.use([
|
|
|
- PieChart,
|
|
|
- BarChart,
|
|
|
- LineChart,
|
|
|
- GridComponent,
|
|
|
- TitleComponent,
|
|
|
- LegendComponent,
|
|
|
- TooltipComponent,
|
|
|
- DataZoomComponent,
|
|
|
+ PieChart, BarChart, LineChart,
|
|
|
+ GridComponent, TitleComponent, LegendComponent, TooltipComponent,
|
|
|
CanvasRenderer
|
|
|
]);
|
|
|
import { getDashboard, getTrend, getWashDeviceStatus } from "@/api/stat";
|
|
|
-import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
|
|
|
-import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
|
|
|
|
|
-defineOptions({
|
|
|
- name: "Dashboard"
|
|
|
-});
|
|
|
+defineOptions({ name: "Dashboard" });
|
|
|
|
|
|
const homeLineRef = ref();
|
|
|
const homePieRef = ref();
|
|
|
-const { dataTheme } = useDataThemeChange();
|
|
|
|
|
|
const end = new Date();
|
|
|
const start = new Date();
|
|
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
|
|
|
|
|
const fmtMoney = (value: number) => {
|
|
|
- if (!value) return "0";
|
|
|
- return (value / 100).toFixed(2);
|
|
|
+ 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) => {
|
|
|
@@ -51,50 +44,31 @@ const formatDate = (date: Date) => {
|
|
|
};
|
|
|
|
|
|
const dateDiff = (start: Date, end: Date) => {
|
|
|
- const diffTime = Math.abs(end.getTime() - start.getTime());
|
|
|
- return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
+ 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;
|
|
|
- },
|
|
|
- set: (key: string, value: any) => {
|
|
|
- sessionStorage.setItem(key, JSON.stringify(value));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 品牌衍生图表色板
|
|
|
-const CHART_BAR = "#4DA89D";
|
|
|
-const CHART_LINE = "#C83A35";
|
|
|
-const CHART_GRID = "#EBEBEB";
|
|
|
-
|
|
|
-const PIE_COLORS = [
|
|
|
- "#C83A35",
|
|
|
- "#4DA89D",
|
|
|
- "#E5A350",
|
|
|
- "#8B7EC8",
|
|
|
- "#5BA0D9",
|
|
|
- "#8E8E8E"
|
|
|
-];
|
|
|
-
|
|
|
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,
|
|
|
- dispose: [null, "", undefined]
|
|
|
- },
|
|
|
- metrics: {
|
|
|
- registeredMembers: { value: "0", label: "今日注册会员", icon: "ri/user-add-line" },
|
|
|
- todayIncome: { value: "0", label: "今日收益金额", icon: "ri/money-dollar-circle-line" },
|
|
|
- consumptionAmount: { value: "0", label: "今日消费总额", icon: "ri/shopping-cart-2-line" },
|
|
|
- avgOrderPrice: { value: "0", label: "订单均价", icon: "ri/calculator-line" },
|
|
|
- todayOrders: { value: "0", label: "今日订单数量", icon: "ri/file-list-3-line" },
|
|
|
- avgDuration: { value: "0", label: "洗车平均时长", icon: "ri/timer-line" }
|
|
|
+ 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: "",
|
|
|
@@ -138,7 +112,7 @@ const shortcuts = [
|
|
|
];
|
|
|
|
|
|
const initLineChart = (dataList: Array<any>) => {
|
|
|
- if (!state.global.dispose.some((b: any) => b === state.global.homeChartOne)) {
|
|
|
+ if (!state.myCharts.includes(state.global.homeChartOne)) {
|
|
|
state.global.homeChartOne?.dispose();
|
|
|
}
|
|
|
state.global.homeChartOne = markRaw(echarts.init(homeLineRef.value, state.charts.theme));
|
|
|
@@ -153,26 +127,26 @@ const initLineChart = (dataList: Array<any>) => {
|
|
|
|
|
|
dataList.sort((a, b) => a.seq - b.seq);
|
|
|
|
|
|
- const xAxis = dataList.map(k => k.startTime);
|
|
|
const option = {
|
|
|
backgroundColor: state.charts.bgColor,
|
|
|
title: {
|
|
|
text: "洗车数据走势图",
|
|
|
- x: "left",
|
|
|
- textStyle: { fontSize: "15", color: state.charts.color }
|
|
|
+ left: 0,
|
|
|
+ textStyle: { fontSize: 15, color: state.charts.color }
|
|
|
},
|
|
|
- grid: { top: 70, right: 0, bottom: 30, left: 50 },
|
|
|
+ grid: { top: 50, right: 20, bottom: 30, left: 60 },
|
|
|
tooltip: { trigger: "axis" },
|
|
|
- legend: { data: ["洗车量", "总金额"], right: 20 },
|
|
|
+ legend: { data: ["洗车量", "总金额"], right: 0 },
|
|
|
xAxis: {
|
|
|
- data: xAxis
|
|
|
+ data: dataList.map(k => k.startTime),
|
|
|
+ axisLine: { lineStyle: { color: state.charts.color } }
|
|
|
},
|
|
|
yAxis: [
|
|
|
{
|
|
|
type: "value",
|
|
|
name: "费用/元 洗车量/次",
|
|
|
position: "left",
|
|
|
- splitLine: { show: true, lineStyle: { type: "dashed", color: CHART_GRID } }
|
|
|
+ splitLine: { show: true, lineStyle: { type: "dashed", color: "#f0f0f0" } }
|
|
|
}
|
|
|
],
|
|
|
series: [
|
|
|
@@ -180,12 +154,13 @@ const initLineChart = (dataList: Array<any>) => {
|
|
|
name: "洗车量",
|
|
|
type: "bar",
|
|
|
barWidth: 10,
|
|
|
- symbolSize: 6,
|
|
|
- symbol: "circle",
|
|
|
smooth: true,
|
|
|
data: dataList.map(k => k.totalOrders),
|
|
|
- lineStyle: { color: CHART_BAR },
|
|
|
- itemStyle: { color: CHART_BAR, borderColor: CHART_BAR, barBorderRadius: 5 }
|
|
|
+ itemStyle: {
|
|
|
+ color: "#68a7a0",
|
|
|
+ borderColor: "#68a7a0",
|
|
|
+ barBorderRadius: 5
|
|
|
+ }
|
|
|
},
|
|
|
{
|
|
|
name: "总金额",
|
|
|
@@ -194,8 +169,8 @@ const initLineChart = (dataList: Array<any>) => {
|
|
|
symbol: "circle",
|
|
|
smooth: true,
|
|
|
data: dataList.map(k => fmtMoney(k.totalAmount)),
|
|
|
- lineStyle: { color: CHART_LINE },
|
|
|
- itemStyle: { color: CHART_LINE, borderColor: CHART_LINE }
|
|
|
+ lineStyle: { color: "#409EFF" },
|
|
|
+ itemStyle: { color: "#409EFF", borderColor: "#409EFF" }
|
|
|
}
|
|
|
]
|
|
|
};
|
|
|
@@ -204,7 +179,7 @@ const initLineChart = (dataList: Array<any>) => {
|
|
|
};
|
|
|
|
|
|
const initPieChart = (dataMap: any) => {
|
|
|
- if (!state.global.dispose.some((b: any) => b === state.global.homeChartTwo)) {
|
|
|
+ if (!state.myCharts.includes(state.global.homeChartTwo)) {
|
|
|
state.global.homeChartTwo?.dispose();
|
|
|
}
|
|
|
state.global.homeChartTwo = markRaw(echarts.init(homePieRef.value, state.charts.theme));
|
|
|
@@ -217,6 +192,7 @@ const initPieChart = (dataMap: any) => {
|
|
|
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++) {
|
|
|
@@ -228,15 +204,15 @@ const initPieChart = (dataMap: any) => {
|
|
|
backgroundColor: state.charts.bgColor,
|
|
|
title: {
|
|
|
text: "洗车设备状态",
|
|
|
- x: "left",
|
|
|
- textStyle: { fontSize: "15", color: state.charts.color }
|
|
|
+ left: 0,
|
|
|
+ textStyle: { fontSize: 15, color: state.charts.color }
|
|
|
},
|
|
|
- tooltip: { trigger: "item", formatter: "{b} <br/> {c}" },
|
|
|
+ tooltip: { trigger: "item", formatter: "{b}<br/>{c} 台" },
|
|
|
legend: {
|
|
|
type: "scroll",
|
|
|
orient: "vertical",
|
|
|
- right: "0%",
|
|
|
- left: "65%",
|
|
|
+ right: 0,
|
|
|
+ left: "62%",
|
|
|
top: "center",
|
|
|
itemWidth: 14,
|
|
|
itemHeight: 14,
|
|
|
@@ -246,14 +222,14 @@ const initPieChart = (dataMap: any) => {
|
|
|
series: [
|
|
|
{
|
|
|
type: "pie",
|
|
|
- radius: ["82", dataTheme.value ? "50" : "102"],
|
|
|
- center: ["32%", "50%"],
|
|
|
+ radius: ["82", "102"],
|
|
|
+ center: ["30%", "50%"],
|
|
|
itemStyle: {
|
|
|
- color: (params: any) => PIE_COLORS[params.dataIndex % PIE_COLORS.length]
|
|
|
+ color: (params: any) => colorList[params.dataIndex % colorList.length]
|
|
|
},
|
|
|
label: { show: false },
|
|
|
labelLine: { show: false },
|
|
|
- data: data
|
|
|
+ data
|
|
|
}
|
|
|
]
|
|
|
};
|
|
|
@@ -261,20 +237,14 @@ const initPieChart = (dataMap: any) => {
|
|
|
state.myCharts.push(state.global.homeChartTwo);
|
|
|
};
|
|
|
|
|
|
-const initEchartsResizeFun = () => {
|
|
|
+const initEchartsResize = () => {
|
|
|
nextTick(() => {
|
|
|
for (let i = 0; i < state.myCharts.length; i++) {
|
|
|
- setTimeout(() => {
|
|
|
- state.myCharts[i]?.resize();
|
|
|
- }, i * 200);
|
|
|
+ state.myCharts[i]?.resize();
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-const initEchartsResize = () => {
|
|
|
- window.addEventListener("resize", initEchartsResizeFun);
|
|
|
-};
|
|
|
-
|
|
|
const loadCurrentEquipmentStatus = () => {
|
|
|
getWashDeviceStatus(state.currentStationId || undefined).then((res: any) => {
|
|
|
initPieChart(res?.data || res || {});
|
|
|
@@ -301,152 +271,105 @@ const loadStationStatToday = () => {
|
|
|
getDashboard(state.currentStationId || undefined).then((res: any) => {
|
|
|
const data = res?.data || res;
|
|
|
if (data) {
|
|
|
- state.metrics.registeredMembers.value = String(data.todayRegisteredMembers || 0);
|
|
|
- state.metrics.todayIncome.value = fmtMoney(data.todayIncome || 0);
|
|
|
- state.metrics.consumptionAmount.value = fmtMoney(data.todayConsumptionAmount || 0);
|
|
|
- state.metrics.avgOrderPrice.value = fmtMoney(data.avgOrderPrice || 0);
|
|
|
- state.metrics.todayOrders.value = String(data.todayWashOrders || 0);
|
|
|
- state.metrics.avgDuration.value = ((data.avgOrderDuration || 0) / 60).toFixed(1);
|
|
|
+ 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(() => {
|
|
|
const currentStationId = Session.get("currentStationId");
|
|
|
if (currentStationId) {
|
|
|
state.currentStationId = currentStationId;
|
|
|
- initEchartsResize();
|
|
|
- loadStationStat();
|
|
|
- loadStationStatToday();
|
|
|
- loadCurrentEquipmentStatus();
|
|
|
- } else {
|
|
|
- initEchartsResize();
|
|
|
}
|
|
|
+ window.addEventListener("resize", initEchartsResize);
|
|
|
+ loadAll();
|
|
|
});
|
|
|
|
|
|
onActivated(() => {
|
|
|
- initEchartsResizeFun();
|
|
|
+ initEchartsResize();
|
|
|
});
|
|
|
|
|
|
watch(
|
|
|
- () => dataTheme.value,
|
|
|
- (isDark) => {
|
|
|
+ () => state.charts.theme,
|
|
|
+ () => {
|
|
|
nextTick(() => {
|
|
|
- state.charts.theme = isDark ? "dark" : "";
|
|
|
- state.charts.bgColor = isDark ? "transparent" : "";
|
|
|
- state.charts.color = isDark ? "#C8C8C8" : "#303133";
|
|
|
- setTimeout(() => loadStationStat(), 500);
|
|
|
- setTimeout(() => loadCurrentEquipmentStatus(), 700);
|
|
|
+ state.myCharts = [];
|
|
|
+ setTimeout(() => loadStationStat(), 300);
|
|
|
+ setTimeout(() => loadCurrentEquipmentStatus(), 500);
|
|
|
});
|
|
|
}
|
|
|
);
|
|
|
-
|
|
|
-const featuredMetrics = computed(() => [
|
|
|
- state.metrics.todayIncome,
|
|
|
- state.metrics.todayOrders
|
|
|
-]);
|
|
|
-
|
|
|
-const secondaryMetrics = computed(() => [
|
|
|
- state.metrics.consumptionAmount,
|
|
|
- state.metrics.avgOrderPrice,
|
|
|
- state.metrics.registeredMembers,
|
|
|
- state.metrics.avgDuration
|
|
|
-]);
|
|
|
-
|
|
|
-const isMoneyMetric = (label: string) => {
|
|
|
- return label.includes("金额") || label.includes("收益") || label.includes("均价") || label.includes("消费");
|
|
|
-};
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="dashboard-container">
|
|
|
- <!-- 未选择站点提醒 -->
|
|
|
+ <!-- 未选择站点 -->
|
|
|
<el-alert
|
|
|
v-if="!state.currentStationId"
|
|
|
title="未选择站点"
|
|
|
type="warning"
|
|
|
- description="请通过右上角站点选择器切换到要查看的门店,选择后将自动加载统计数据"
|
|
|
+ description="请通过右上角站点选择器切换到要查看的门店"
|
|
|
show-icon
|
|
|
closable
|
|
|
- class="mb-4"
|
|
|
+ class="mb15"
|
|
|
/>
|
|
|
|
|
|
- <!-- 核心指标 -->
|
|
|
- <div class="featured-row">
|
|
|
- <div
|
|
|
- v-for="(metric, idx) in featuredMetrics"
|
|
|
- :key="idx"
|
|
|
- class="featured-card"
|
|
|
- :class="idx === 0 ? 'featured-primary' : 'featured-secondary'"
|
|
|
- >
|
|
|
- <div class="featured-icon">
|
|
|
- <component :is="useRenderIcon(metric.icon)" />
|
|
|
- </div>
|
|
|
- <div class="featured-body">
|
|
|
- <div class="featured-value">
|
|
|
- {{ metric.value }}
|
|
|
- <span v-if="isMoneyMetric(metric.label)" class="featured-unit">元</span>
|
|
|
- <span v-else class="featured-unit">笔</span>
|
|
|
- </div>
|
|
|
- <div class="featured-label">{{ metric.label }}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 次要指标 -->
|
|
|
- <div class="secondary-row">
|
|
|
- <div
|
|
|
- v-for="(metric, idx) in secondaryMetrics"
|
|
|
- :key="idx"
|
|
|
- class="secondary-card"
|
|
|
+ <!-- 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="secondary-icon">
|
|
|
- <component :is="useRenderIcon(metric.icon)" />
|
|
|
+ <div class="metric-card">
|
|
|
+ <div class="metric-value" :style="{ color: m.color }">{{ m.value }}</div>
|
|
|
+ <div class="metric-label">{{ m.label }}</div>
|
|
|
</div>
|
|
|
- <div class="secondary-body">
|
|
|
- <div class="secondary-value">
|
|
|
- {{ metric.value }}
|
|
|
- <span v-if="isMoneyMetric(metric.label)" class="secondary-unit">元</span>
|
|
|
- <span v-else-if="metric.label.includes('时长')" class="secondary-unit">分钟</span>
|
|
|
- <span v-else class="secondary-unit">人</span>
|
|
|
- </div>
|
|
|
- <div class="secondary-label">{{ metric.label }}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
|
|
|
- <!-- 图表区域 -->
|
|
|
+ <!-- 走势图 + 饼图 -->
|
|
|
<el-row :gutter="15">
|
|
|
<el-col :xs="24" :sm="14" :md="14" :lg="16" :xl="16">
|
|
|
- <el-card class="chart-card">
|
|
|
- <template #header>
|
|
|
- <div class="chart-header">
|
|
|
- <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"
|
|
|
- />
|
|
|
- <div class="chart-summary">
|
|
|
- 总收益:
|
|
|
- <el-tag type="success" size="small">{{ fmtMoney(state.homeOneExtra.totalIncome) }}元</el-tag>
|
|
|
- 总订单:
|
|
|
- <el-tag type="danger" size="small">{{ state.homeOneExtra.totalWashOrders }}笔</el-tag>
|
|
|
- </div>
|
|
|
+ <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>
|
|
|
- </template>
|
|
|
- <div class="chart-wrapper" ref="homeLineRef" />
|
|
|
- </el-card>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body" ref="homeLineRef" />
|
|
|
+ </div>
|
|
|
</el-col>
|
|
|
<el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8">
|
|
|
- <el-card class="chart-card">
|
|
|
- <div class="chart-wrapper" ref="homePieRef" />
|
|
|
- </el-card>
|
|
|
+ <div class="chart-panel">
|
|
|
+ <div class="chart-body" ref="homePieRef" />
|
|
|
+ </div>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
@@ -454,165 +377,72 @@ const isMoneyMetric = (label: string) => {
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.dashboard-container {
|
|
|
- padding: 15px;
|
|
|
+ padding: 20px;
|
|
|
}
|
|
|
|
|
|
-// 核心指标行
|
|
|
-.featured-row {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 3fr 2fr;
|
|
|
- gap: 15px;
|
|
|
+// 指标卡片
|
|
|
+.metrics-row {
|
|
|
margin-bottom: 15px;
|
|
|
-
|
|
|
- @media (max-width: 768px) {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
-.featured-card {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 20px;
|
|
|
- padding: 24px 28px;
|
|
|
- border-radius: 4px;
|
|
|
- background: var(--el-bg-color);
|
|
|
-
|
|
|
- &.featured-primary {
|
|
|
- box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
|
|
|
- border-top: 3px solid var(--el-color-primary);
|
|
|
+.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);
|
|
|
}
|
|
|
|
|
|
- &.featured-secondary {
|
|
|
- box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
|
|
|
+ .metric-value {
|
|
|
+ font-size: 30px;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1.3;
|
|
|
+ letter-spacing: -0.5px;
|
|
|
}
|
|
|
|
|
|
- .featured-icon {
|
|
|
- font-size: 40px;
|
|
|
- color: var(--el-color-primary);
|
|
|
- opacity: 0.85;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .featured-body {
|
|
|
- .featured-value {
|
|
|
- font-size: 36px;
|
|
|
- font-weight: 700;
|
|
|
- line-height: 1.2;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- letter-spacing: -0.5px;
|
|
|
- }
|
|
|
-
|
|
|
- .featured-unit {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 500;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
- margin-left: 4px;
|
|
|
- }
|
|
|
-
|
|
|
- .featured-label {
|
|
|
- margin-top: 6px;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
- }
|
|
|
+ .metric-label {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 次要指标行
|
|
|
-.secondary-row {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(4, 1fr);
|
|
|
- gap: 15px;
|
|
|
+// 图表面板
|
|
|
+.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;
|
|
|
|
|
|
- @media (max-width: 992px) {
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
|
|
}
|
|
|
|
|
|
- @media (max-width: 640px) {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.secondary-card {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 14px;
|
|
|
- padding: 18px 20px;
|
|
|
- border-radius: 4px;
|
|
|
- background: var(--el-bg-color);
|
|
|
- box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
|
|
|
-
|
|
|
- .secondary-icon {
|
|
|
- font-size: 28px;
|
|
|
- color: var(--el-text-color-placeholder);
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .secondary-body {
|
|
|
- min-width: 0;
|
|
|
-
|
|
|
- .secondary-value {
|
|
|
- font-size: 22px;
|
|
|
- font-weight: 600;
|
|
|
- line-height: 1.3;
|
|
|
- color: var(--el-text-color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- .secondary-unit {
|
|
|
- font-size: 12px;
|
|
|
- font-weight: 500;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
- margin-left: 2px;
|
|
|
- }
|
|
|
-
|
|
|
- .secondary-label {
|
|
|
- margin-top: 2px;
|
|
|
- font-size: 13px;
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 图表
|
|
|
-.chart-card {
|
|
|
- margin-bottom: 15px;
|
|
|
-
|
|
|
- .chart-header {
|
|
|
+ .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);
|
|
|
-
|
|
|
- .el-tag {
|
|
|
- margin: 0 4px;
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .chart-wrapper {
|
|
|
- height: 350px;
|
|
|
+ .chart-body {
|
|
|
+ height: 380px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-@media (max-width: 768px) {
|
|
|
- .chart-header {
|
|
|
- flex-direction: column;
|
|
|
- align-items: flex-start;
|
|
|
- }
|
|
|
-
|
|
|
- .featured-card {
|
|
|
- padding: 18px 20px;
|
|
|
-
|
|
|
- .featured-value {
|
|
|
- font-size: 28px;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
</style>
|