851 lines
20 KiB
Vue
851 lines
20 KiB
Vue
<script setup>
|
|
import { ref, onMounted, onUnmounted } from "vue";
|
|
import * as echarts from "echarts";
|
|
import { markRaw } from "vue";
|
|
import {
|
|
Monitor,
|
|
DataAnalysis,
|
|
Location,
|
|
Document,
|
|
Warning,
|
|
} from "@element-plus/icons-vue";
|
|
import { getSpeciesStatistics, getPatrolStatistics, getDeviceList } from "@/api/dashboard";
|
|
|
|
// 使用 markRaw 包装图标组件
|
|
const icons = {
|
|
Monitor: markRaw(Monitor),
|
|
DataAnalysis: markRaw(DataAnalysis),
|
|
Location: markRaw(Location),
|
|
Document: markRaw(Document),
|
|
Warning: markRaw(Warning),
|
|
};
|
|
|
|
// 物种类别选项
|
|
const categoryOptions = [
|
|
{ label: "鸟类", value: "bird" },
|
|
{ label: "哺乳类", value: "mammal" },
|
|
{ label: "鱼类", value: "fish" },
|
|
{ label: "两栖类", value: "amphibian" },
|
|
{ label: "爬行类", value: "reptile" },
|
|
{ label: "昆虫类", value: "insect" },
|
|
{ label: "植物", value: "plant" },
|
|
];
|
|
|
|
// 保护等级选项
|
|
const protectionLevelOptions = [
|
|
{ label: "国家一级", value: "national_first" },
|
|
{ label: "国家二级", value: "national_second" },
|
|
{ label: "省级", value: "provincial" },
|
|
{ label: "普通", value: "normal" },
|
|
];
|
|
|
|
// 图表实例
|
|
const categoryChartRef = ref(null);
|
|
let categoryChart = null;
|
|
const protectionChartRef = ref(null);
|
|
let protectionChart = null;
|
|
const trendChartRef = ref(null);
|
|
|
|
// 统计卡片数据
|
|
const statsCards = ref([
|
|
{
|
|
title: "物种监测",
|
|
icon: icons.Monitor,
|
|
value: "0",
|
|
unit: "种",
|
|
change: { value: "0", label: "今日新增" },
|
|
color: "#1890FF",
|
|
bgColor: "linear-gradient(120deg, #0072FF 0%, #00C6FF 100%)",
|
|
features: ["实时监测", "智能识别", "行为分析", "分布追踪"],
|
|
},
|
|
{
|
|
title: "环境监测",
|
|
icon: icons.DataAnalysis,
|
|
value: "2",
|
|
unit: "点",
|
|
change: { value: "2", label: "异常" },
|
|
color: "#F5222D",
|
|
bgColor: "linear-gradient(120deg, #FF416C 0%, #FF4B2B 100%)",
|
|
features: ["水质监测", "空气监测", "土壤监测", "气象监测"],
|
|
},
|
|
{
|
|
title: "巡护任务",
|
|
icon: icons.Location,
|
|
value: "0",
|
|
unit: "个",
|
|
change: { value: "0%", label: "完成率" },
|
|
color: "#52C41A",
|
|
bgColor: "linear-gradient(120deg, #00B09B 0%, #96C93D 100%)",
|
|
features: ["智能派单", "轨迹记录", "实时通讯", "数据采集"],
|
|
},
|
|
{
|
|
title: "设备状态",
|
|
icon: icons.Monitor,
|
|
value: "0",
|
|
unit: "台",
|
|
change: { value: "0%", label: "在线率" },
|
|
color: "#722ED1",
|
|
bgColor: "linear-gradient(120deg, #7F00FF 0%, #E100FF 100%)",
|
|
features: ["状态监控", "故障预警", "维护管理", "性能分析"],
|
|
},
|
|
]);
|
|
|
|
// 设备状态统计数据
|
|
const deviceData = ref({
|
|
total: 0,
|
|
online: 0,
|
|
});
|
|
|
|
// 初始化物种类别图表
|
|
const initCategoryChart = () => {
|
|
if (!categoryChartRef.value) return;
|
|
|
|
categoryChart = echarts.init(categoryChartRef.value);
|
|
const option = {
|
|
tooltip: {
|
|
trigger: "item",
|
|
formatter: "{b}: {c}种 ({d}%)",
|
|
},
|
|
legend: {
|
|
orient: "vertical",
|
|
left: "left",
|
|
top: "middle",
|
|
textStyle: {
|
|
color: "#303133",
|
|
},
|
|
},
|
|
series: [
|
|
{
|
|
name: "物种数量",
|
|
type: "pie",
|
|
radius: ["40%", "70%"],
|
|
center: ["60%", "50%"],
|
|
avoidLabelOverlap: true,
|
|
itemStyle: {
|
|
borderRadius: 10,
|
|
borderColor: "#fff",
|
|
borderWidth: 2,
|
|
},
|
|
label: {
|
|
show: true,
|
|
formatter: "{b}: {c}种",
|
|
},
|
|
emphasis: {
|
|
label: {
|
|
show: true,
|
|
fontSize: 14,
|
|
fontWeight: "bold",
|
|
},
|
|
itemStyle: {
|
|
shadowBlur: 10,
|
|
shadowOffsetX: 0,
|
|
shadowColor: "rgba(0, 0, 0, 0.5)",
|
|
},
|
|
},
|
|
data: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
categoryChart.setOption(option);
|
|
};
|
|
|
|
// 更新图表数据
|
|
const updateCategoryChart = (data) => {
|
|
if (!categoryChart) return;
|
|
|
|
// 物种类别图表数据
|
|
const categoryData = Object.entries(data.categories)
|
|
.filter(([_, count]) => count.total_count > 0)
|
|
.map(([category, count]) => ({
|
|
name: categoryOptions.find((item) => item.value === category)?.label || category,
|
|
value: parseInt(count.total_count),
|
|
}))
|
|
.sort((a, b) => b.value - a.value);
|
|
|
|
categoryChart.setOption({
|
|
series: [
|
|
{
|
|
data: categoryData,
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
// 获取物种统计数据
|
|
const fetchSpeciesData = async () => {
|
|
try {
|
|
const res = await getSpeciesStatistics();
|
|
if (res.success && res.data) {
|
|
// 计算总物种数和今日新增数
|
|
const totalSpecies = Object.values(res.data.categories).reduce(
|
|
(sum, category) => sum + (parseInt(category.total_count) || 0),
|
|
0
|
|
);
|
|
const todayNew = Object.values(res.data.categories).reduce(
|
|
(sum, category) => sum + (parseInt(category.today_count) || 0),
|
|
0
|
|
);
|
|
|
|
// 更新物种监测卡片
|
|
statsCards.value[0].value = String(totalSpecies || 0);
|
|
statsCards.value[0].change.value = `+${todayNew || 0}`;
|
|
|
|
// 更新物种分布图表
|
|
updateCategoryChart(res.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("获取物种统计数据失败:", error);
|
|
}
|
|
};
|
|
|
|
// 获取巡护任务统计数据
|
|
const fetchPatrolData = async () => {
|
|
try {
|
|
const res = await getPatrolStatistics();
|
|
if (res.success && res.data) {
|
|
const { overview } = res.data;
|
|
const progress = ((overview.completed_count / overview.total_count) * 100).toFixed(1);
|
|
|
|
// 更新巡护任务卡片
|
|
statsCards.value[2].value = String(overview.total_count);
|
|
statsCards.value[2].change.value = `${progress}%`;
|
|
}
|
|
} catch (error) {
|
|
console.error("获取巡护任务统计数据失败:", error);
|
|
}
|
|
};
|
|
|
|
// 获取设备列表数据
|
|
const fetchDeviceData = async () => {
|
|
try {
|
|
const res = await getDeviceList();
|
|
if (res.success && res.data?.list) {
|
|
const deviceList = res.data.list;
|
|
deviceData.value = {
|
|
total: deviceList.length,
|
|
online: deviceList.filter((device) => device.status?.code === 1).length,
|
|
};
|
|
|
|
// 更新设备状态卡片
|
|
statsCards.value[3].value = String(deviceData.value.total);
|
|
statsCards.value[3].change.value = `${(
|
|
(deviceData.value.online / deviceData.value.total) *
|
|
100
|
|
).toFixed(1)}%`;
|
|
// 更新设备状态特性
|
|
statsCards.value[3].features = [
|
|
`在线: ${deviceData.value.online}台`,
|
|
`离线: ${deviceData.value.total - deviceData.value.online}台`,
|
|
"故障预警",
|
|
"性能分析",
|
|
];
|
|
}
|
|
} catch (error) {
|
|
console.error("获取设备列表数据失败:", error);
|
|
}
|
|
};
|
|
|
|
// 初始化保护等级图表
|
|
const initProtectionChart = () => {
|
|
if (!protectionChartRef.value) return;
|
|
|
|
protectionChart = echarts.init(protectionChartRef.value);
|
|
const option = {
|
|
title: {
|
|
textStyle: {
|
|
fontSize: 16,
|
|
fontWeight: 500,
|
|
color: "#303133",
|
|
},
|
|
},
|
|
tooltip: {
|
|
trigger: "axis",
|
|
axisPointer: {
|
|
type: "shadow",
|
|
},
|
|
formatter: "{b}: {c}种",
|
|
},
|
|
grid: {
|
|
left: "3%",
|
|
right: "4%",
|
|
bottom: "10%",
|
|
containLabel: true,
|
|
},
|
|
xAxis: {
|
|
type: "category",
|
|
data: [],
|
|
axisLabel: {
|
|
interval: 0,
|
|
rotate: 30,
|
|
},
|
|
},
|
|
yAxis: {
|
|
type: "value",
|
|
name: "物种数量",
|
|
minInterval: 1,
|
|
},
|
|
series: [
|
|
{
|
|
name: "物种数量",
|
|
type: "bar",
|
|
barWidth: "40%",
|
|
data: [],
|
|
label: {
|
|
show: true,
|
|
position: "top",
|
|
formatter: "{c}种",
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
protectionChart.setOption(option);
|
|
};
|
|
|
|
// 更新保护等级图表数据
|
|
const updateProtectionChart = (data) => {
|
|
if (!protectionChart) return;
|
|
|
|
// 保护等级图表数据
|
|
const protectionData = Object.entries(data.protection_levels)
|
|
.filter(([_, count]) => count > 0)
|
|
.map(([level, count]) => ({
|
|
name: protectionLevelOptions.find((item) => item.value === level)?.label || level,
|
|
value: count,
|
|
}))
|
|
.sort((a, b) => b.value - a.value);
|
|
|
|
protectionChart.setOption({
|
|
xAxis: {
|
|
data: protectionData.map((item) => item.name),
|
|
},
|
|
series: [
|
|
{
|
|
data: protectionData.map((item) => ({
|
|
value: item.value,
|
|
itemStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: "#83bff6" },
|
|
{ offset: 0.5, color: "#409EFF" },
|
|
{ offset: 1, color: "#2c76c5" },
|
|
]),
|
|
},
|
|
})),
|
|
},
|
|
],
|
|
});
|
|
};
|
|
|
|
// 初始化数据
|
|
const initData = async () => {
|
|
try {
|
|
await Promise.all([fetchSpeciesData(), fetchPatrolData(), fetchDeviceData()]);
|
|
} catch (error) {
|
|
console.error("初始化数据失败:", error);
|
|
}
|
|
};
|
|
|
|
// 自动刷新数据
|
|
let timer = null;
|
|
const startAutoRefresh = () => {
|
|
fetchStatisticsData();
|
|
timer = setInterval(fetchStatisticsData, 60000); // 每分钟更新一次
|
|
};
|
|
|
|
// 初始化趋势图表
|
|
const initTrendChart = () => {
|
|
const chartDom = document.getElementById("trendChart");
|
|
if (!chartDom) return;
|
|
|
|
const myChart = echarts.init(chartDom);
|
|
const option = {
|
|
title: {
|
|
textStyle: {
|
|
fontSize: 16,
|
|
fontWeight: 500,
|
|
color: "#303133",
|
|
},
|
|
},
|
|
tooltip: {
|
|
trigger: "axis",
|
|
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
borderColor: "#eee",
|
|
padding: [10, 15],
|
|
textStyle: {
|
|
color: "#666",
|
|
},
|
|
},
|
|
legend: {
|
|
data: ["物种数量", "监测数据"],
|
|
right: 20,
|
|
top: 10,
|
|
textStyle: {
|
|
color: "#666",
|
|
},
|
|
itemWidth: 12,
|
|
itemHeight: 12,
|
|
itemGap: 20,
|
|
},
|
|
grid: {
|
|
left: "3%",
|
|
right: "4%",
|
|
bottom: "3%",
|
|
containLabel: true,
|
|
},
|
|
xAxis: {
|
|
type: "category",
|
|
boundaryGap: false,
|
|
data: ["2-14", "2-15", "2-16", "2-17", "2-18", "2-19", "2-20"],
|
|
axisLine: {
|
|
lineStyle: {
|
|
color: "#DCDFE6",
|
|
},
|
|
},
|
|
axisTick: {
|
|
show: false,
|
|
},
|
|
axisLabel: {
|
|
color: "#909399",
|
|
},
|
|
},
|
|
yAxis: {
|
|
type: "value",
|
|
max: 50,
|
|
interval: 10,
|
|
axisLine: {
|
|
show: false,
|
|
},
|
|
axisTick: {
|
|
show: false,
|
|
},
|
|
splitLine: {
|
|
lineStyle: {
|
|
color: "#EBEEF5",
|
|
type: "dashed",
|
|
},
|
|
},
|
|
axisLabel: {
|
|
color: "#909399",
|
|
},
|
|
},
|
|
series: [
|
|
{
|
|
name: "物种数量",
|
|
type: "line",
|
|
smooth: true,
|
|
symbolSize: 8,
|
|
lineStyle: {
|
|
width: 3,
|
|
color: "#409EFF",
|
|
},
|
|
itemStyle: {
|
|
color: "#409EFF",
|
|
borderColor: "#fff",
|
|
borderWidth: 2,
|
|
},
|
|
areaStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: "rgba(64, 158, 255, 0.2)" },
|
|
{ offset: 1, color: "rgba(64, 158, 255, 0)" },
|
|
]),
|
|
},
|
|
emphasis: {
|
|
itemStyle: {
|
|
borderWidth: 3,
|
|
shadowColor: "rgba(64, 158, 255, 0.5)",
|
|
shadowBlur: 10,
|
|
},
|
|
},
|
|
data: [10, 12, 15, 13, 18, 15, 20],
|
|
},
|
|
{
|
|
name: "监测数据",
|
|
type: "line",
|
|
smooth: true,
|
|
symbolSize: 8,
|
|
lineStyle: {
|
|
width: 3,
|
|
color: "#67C23A",
|
|
},
|
|
itemStyle: {
|
|
color: "#67C23A",
|
|
borderColor: "#fff",
|
|
borderWidth: 2,
|
|
},
|
|
areaStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: "rgba(103, 194, 58, 0.2)" },
|
|
{ offset: 1, color: "rgba(103, 194, 58, 0)" },
|
|
]),
|
|
},
|
|
emphasis: {
|
|
itemStyle: {
|
|
borderWidth: 3,
|
|
shadowColor: "rgba(103, 194, 58, 0.5)",
|
|
shadowBlur: 10,
|
|
},
|
|
},
|
|
data: [15, 18, 12, 20, 15, 18, 16],
|
|
},
|
|
],
|
|
};
|
|
|
|
myChart.setOption(option);
|
|
window.addEventListener("resize", () => {
|
|
myChart.resize();
|
|
});
|
|
};
|
|
|
|
// 初始化分布图表
|
|
const initDistributionChart = () => {
|
|
const chartDom = document.getElementById("distributionChart");
|
|
if (!chartDom) return;
|
|
|
|
const myChart = echarts.init(chartDom);
|
|
const option = {
|
|
title: {
|
|
text: "物种分布统计",
|
|
textStyle: {
|
|
fontSize: 16,
|
|
fontWeight: 500,
|
|
color: "#2c3e50",
|
|
},
|
|
},
|
|
tooltip: {
|
|
trigger: "item",
|
|
backgroundColor: "rgba(255,255,255,0.9)",
|
|
borderColor: "#eee",
|
|
textStyle: {
|
|
color: "#666",
|
|
},
|
|
formatter: "{b}: {c} ({d}%)",
|
|
},
|
|
legend: {
|
|
bottom: "0%",
|
|
itemWidth: 10,
|
|
itemHeight: 10,
|
|
textStyle: {
|
|
color: "#666",
|
|
fontSize: 12,
|
|
},
|
|
},
|
|
series: [
|
|
{
|
|
name: "物种分布",
|
|
type: "pie",
|
|
radius: ["40%", "70%"],
|
|
center: ["50%", "45%"],
|
|
avoidLabelOverlap: false,
|
|
itemStyle: {
|
|
borderRadius: 6,
|
|
borderColor: "#fff",
|
|
borderWidth: 2,
|
|
},
|
|
label: {
|
|
show: false,
|
|
position: "center",
|
|
},
|
|
emphasis: {
|
|
label: {
|
|
show: true,
|
|
fontSize: 20,
|
|
fontWeight: "bold",
|
|
},
|
|
},
|
|
labelLine: {
|
|
show: false,
|
|
},
|
|
data: [
|
|
{
|
|
value: 38,
|
|
name: "鸟类",
|
|
itemStyle: { color: "#409EFF" },
|
|
},
|
|
{
|
|
value: 25,
|
|
name: "鱼类",
|
|
itemStyle: { color: "#67C23A" },
|
|
},
|
|
{
|
|
value: 22,
|
|
name: "两栖类",
|
|
itemStyle: { color: "#E6A23C" },
|
|
},
|
|
{
|
|
value: 28,
|
|
name: "植物",
|
|
itemStyle: { color: "#F56C6C" },
|
|
},
|
|
{
|
|
value: 15,
|
|
name: "其他",
|
|
itemStyle: { color: "#909399" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
myChart.setOption(option);
|
|
|
|
// 添加鼠标悬停效果
|
|
myChart.on("mouseover", { seriesIndex: 0 }, function (params) {
|
|
myChart.dispatchAction({
|
|
type: "highlight",
|
|
dataIndex: params.dataIndex,
|
|
});
|
|
});
|
|
|
|
myChart.on("mouseout", { seriesIndex: 0 }, function (params) {
|
|
myChart.dispatchAction({
|
|
type: "downplay",
|
|
dataIndex: params.dataIndex,
|
|
});
|
|
});
|
|
};
|
|
|
|
// 获取统计数据
|
|
const fetchStatisticsData = async () => {
|
|
try {
|
|
const res = await getSpeciesStatistics();
|
|
if (res.success && res.data) {
|
|
// 更新物种总数卡片
|
|
const totalSpecies = Object.values(res.data.categories).reduce(
|
|
(sum, item) => sum + (parseInt(item.total_count) || 0),
|
|
0
|
|
);
|
|
const newSpecies = Object.values(res.data.categories).reduce(
|
|
(sum, item) => sum + (parseInt(item.today_count) || 0),
|
|
0
|
|
);
|
|
statsCards.value[0].value = String(totalSpecies || 0);
|
|
statsCards.value[0].change.value = `+${newSpecies || 0}`;
|
|
|
|
// 更新物种分布图表
|
|
updateCategoryChart(res.data);
|
|
|
|
// 更新保护等级图表
|
|
updateProtectionChart(res.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("获取统计数据失败:", error);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
initData();
|
|
startAutoRefresh();
|
|
initCategoryChart();
|
|
initProtectionChart();
|
|
initTrendChart();
|
|
initDistributionChart();
|
|
window.addEventListener("resize", () => {
|
|
categoryChart?.resize();
|
|
protectionChart?.resize();
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (timer) {
|
|
clearInterval(timer);
|
|
}
|
|
if (categoryChart) {
|
|
categoryChart.dispose();
|
|
categoryChart = null;
|
|
}
|
|
if (protectionChart) {
|
|
protectionChart.dispose();
|
|
protectionChart = null;
|
|
}
|
|
window.removeEventListener("resize", () => {
|
|
categoryChart?.resize();
|
|
protectionChart?.resize();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="dashboard-container">
|
|
<div class="stats-grid">
|
|
<div
|
|
v-for="card in statsCards"
|
|
:key="card.title"
|
|
class="stats-card"
|
|
:style="{ backgroundColor: card.bgColor }"
|
|
>
|
|
<div class="card-header">
|
|
<div class="title">
|
|
<el-icon :size="20" :color="card.color">
|
|
<component :is="card.icon" />
|
|
</el-icon>
|
|
<span>{{ card.title }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="main-value">
|
|
{{ card.value }}
|
|
<span class="unit">{{ card.unit }}</span>
|
|
</div>
|
|
<div
|
|
class="change-value"
|
|
:style="{
|
|
color: card.change.value.includes('+')
|
|
? '#67C23A'
|
|
: card.change.value.includes('%')
|
|
? card.color
|
|
: '#F56C6C',
|
|
}"
|
|
>
|
|
{{ card.change.value }}
|
|
<span class="label">{{ card.change.label }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div v-for="feature in card.features" :key="feature" class="feature-item">
|
|
{{ feature }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表区域 -->
|
|
<div class="charts-container">
|
|
<div class="chart-item">
|
|
<div class="chart-title">物种类别统计</div>
|
|
<div ref="categoryChartRef" class="chart-content"></div>
|
|
</div>
|
|
<div class="chart-item">
|
|
<div class="chart-title">趋势统计</div>
|
|
<div id="trendChart" class="chart-content"></div>
|
|
</div>
|
|
<div class="chart-item">
|
|
<div class="chart-title">保护等级统计</div>
|
|
<div ref="protectionChartRef" class="chart-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
@use "../../styles/variables" as v;
|
|
|
|
.dashboard-container {
|
|
padding-bottom: 24px;
|
|
|
|
.welcome-section {
|
|
margin-bottom: 24px;
|
|
|
|
h2 {
|
|
margin: 0;
|
|
font-size: 24px;
|
|
font-weight: 500;
|
|
color: v.$text-primary;
|
|
}
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 20px;
|
|
|
|
.stats-card {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
transition: all 0.3s ease;
|
|
|
|
&:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.card-header {
|
|
margin-bottom: 16px;
|
|
|
|
.title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 16px;
|
|
color: #606266;
|
|
|
|
.el-icon {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-body {
|
|
margin-bottom: 16px;
|
|
|
|
.main-value {
|
|
font-size: 32px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
margin-bottom: 8px;
|
|
|
|
.unit {
|
|
font-size: 14px;
|
|
font-weight: normal;
|
|
margin-left: 4px;
|
|
color: #909399;
|
|
}
|
|
}
|
|
|
|
.change-value {
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
|
|
.label {
|
|
color: #909399;
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-footer {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 8px;
|
|
|
|
.feature-item {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
padding: 4px 8px;
|
|
background: rgba(255, 255, 255, 0.7);
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.charts-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
margin-bottom: 20px;
|
|
|
|
.chart-item {
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
|
|
.chart-title {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: #303133;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.chart-content {
|
|
height: 400px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|