2025-02-20 20:44:31 +08:00

664 lines
15 KiB
Vue

<script setup>
import { ref, reactive, onMounted } from "vue";
import * as echarts from "echarts";
import { ElMessage } from "element-plus";
import { Monitor, Warning, TrendCharts } from "@element-plus/icons-vue";
const tableData = ref([
{
id: 1,
location: "A区监测点",
temperature: 25.6,
humidity: 65,
waterQuality: "优",
time: "2024-03-20 10:30",
},
{
id: 2,
location: "B区监测点",
temperature: 26.2,
humidity: 62,
waterQuality: "良",
time: "2024-03-20 11:20",
},
]);
// 环境指标数据
const envStats = ref([
{
label: "水质指数",
value: 92.5,
status: "优",
type: "success",
trend: [85, 88, 90, 92.5], // 用于迷你图表
},
{
label: "空气质量",
value: 76.8,
status: "良好",
type: "success",
trend: [75, 72, 78, 76.8],
},
{
label: "温度(°C)",
value: 25.6,
status: "正常",
type: "success",
trend: [24, 25, 26, 25.6],
},
{
label: "湿度(%)",
value: 65,
status: "正常",
type: "success",
trend: [62, 64, 66, 65],
},
]);
// 时间范围选择
const timeRange = ref("24h");
// 初始化图表
const initChart = () => {
const chartDom = document.getElementById("envChart");
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
title: {
text: "环境监测趋势",
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",
},
formatter: function (params) {
let result = `${params[0].axisValue}<br/>`;
params.forEach((item) => {
result += `${item.marker} ${item.seriesName}: ${item.value}${
item.seriesName.includes("温度")
? "°C"
: item.seriesName.includes("湿度")
? "%"
: ""
}<br/>`;
});
return result;
},
},
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: [
"00:00",
"03:00",
"06:00",
"09:00",
"12:00",
"15:00",
"18:00",
"21:00",
"24:00",
],
axisLine: {
lineStyle: {
color: "#DCDFE6",
},
},
axisTick: {
show: false,
},
axisLabel: {
color: "#909399",
formatter: "{value}",
},
},
yAxis: [
{
type: "value",
name: "温度/湿度",
nameTextStyle: {
color: "#909399",
padding: [0, 30, 0, 0],
},
splitLine: {
lineStyle: {
color: "#EBEEF5",
type: "dashed",
},
},
axisLabel: {
color: "#909399",
formatter: "{value}",
},
},
{
type: "value",
name: "指数",
nameTextStyle: {
color: "#909399",
padding: [0, 0, 0, 30],
},
splitLine: {
show: false,
},
axisLabel: {
color: "#909399",
formatter: "{value}",
},
},
],
series: [
{
name: "温度",
type: "line",
smooth: true,
lineStyle: {
width: 3,
color: "#409EFF",
},
itemStyle: {
color: "#409EFF",
},
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)" },
]),
},
data: [22, 23, 24, 25, 26, 27, 26, 25, 24],
},
{
name: "湿度",
type: "line",
smooth: true,
lineStyle: {
width: 3,
color: "#67C23A",
},
itemStyle: {
color: "#67C23A",
},
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)" },
]),
},
data: [60, 62, 65, 63, 65, 68, 67, 65, 64],
},
{
name: "水质指数",
type: "line",
yAxisIndex: 1,
smooth: true,
lineStyle: {
width: 3,
color: "#36CFC9",
},
itemStyle: {
color: "#36CFC9",
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(54, 207, 201, 0.2)" },
{ offset: 1, color: "rgba(54, 207, 201, 0)" },
]),
},
data: [90, 91, 92, 92, 93, 92, 92, 91, 92],
},
],
};
myChart.setOption(option);
// 处理窗口大小变化
window.addEventListener("resize", () => {
myChart.resize();
});
};
// 导出相关数据
const exportDialogVisible = ref(false);
const exportForm = reactive({
timeRange: [],
dataType: ["temperature", "humidity", "waterQuality"],
format: "excel",
});
// 处理导出确认
const handleExportConfirm = () => {
ElMessage.success("数据导出成功");
exportDialogVisible.value = false;
};
// 初始化迷你图表
const initMiniChart = (el, data) => {
const chart = echarts.init(el);
const option = {
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
xAxis: {
type: "category",
show: false,
boundaryGap: false,
},
yAxis: {
type: "value",
show: false,
scale: true,
},
series: [
{
data: data,
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 0,
sampling: "average",
showSymbol: false,
emphasis: {
focus: "series",
showSymbol: true,
symbolSize: 4,
},
lineStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#409EFF",
},
{
offset: 1,
color: "#36CFFB",
},
],
},
width: 2,
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "rgba(64,158,255,0.25)",
},
{
offset: 1,
color: "rgba(54,207,251,0.05)",
},
],
},
},
},
],
tooltip: {
trigger: "axis",
formatter: "{c}",
backgroundColor: "rgba(255,255,255,0.9)",
borderColor: "#eee",
borderWidth: 1,
textStyle: {
color: "#666",
fontSize: 12,
},
padding: [4, 8],
},
};
chart.setOption(option);
// 监听卡片hover事件
const card = el.closest(".env-card");
if (card) {
card.addEventListener("mouseenter", () => {
chart.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: data.length - 1,
});
});
card.addEventListener("mouseleave", () => {
chart.dispatchAction({
type: "hideTip",
});
});
}
};
onMounted(() => {
initChart();
// 初始化所有迷你图表
const chartEls = document.querySelectorAll(".env-chart");
chartEls.forEach((el, index) => {
initMiniChart(el, envStats.value[index].trend);
});
});
</script>
<template>
<div class="env-container">
<!-- 环境指标卡片 -->
<el-row :gutter="20" class="mb-20">
<el-col :span="6" v-for="(item, index) in envStats" :key="index">
<div class="env-card">
<div class="env-header">
<span class="label">{{ item.label }}</span>
<el-tag :type="item.type" effect="plain">{{ item.status }}</el-tag>
</div>
<div class="env-value">{{ item.value }}</div>
<div class="env-chart" :id="`miniChart${index}`"></div>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<div id="envChart" style="height: 400px"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="mt-20">
<el-col :span="24">
<el-card>
<template #header>
<div class="card-header">
<span>实时监测数据</span>
<div>
<el-button type="primary" @click="handleExport">导出数据</el-button>
</div>
</div>
</template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="location" label="监测点" min-width="120" />
<el-table-column prop="temperature" label="温度(°C)" width="120" />
<el-table-column prop="humidity" label="湿度(%)" width="120" />
<el-table-column prop="waterQuality" label="水质" width="100">
<template #default="{ row }">
<el-tag :type="row.waterQuality === '优' ? 'success' : 'warning'">
{{ row.waterQuality }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="time" label="监测时间" min-width="180" />
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 添加导出弹窗 -->
<el-dialog v-model="exportDialogVisible" title="导出数据" width="500px">
<el-form :model="exportForm" label-width="80px">
<el-form-item label="时间范围">
<el-date-picker
v-model="exportForm.timeRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="数据类型">
<el-checkbox-group v-model="exportForm.dataType">
<el-checkbox label="temperature">温度</el-checkbox>
<el-checkbox label="humidity">湿度</el-checkbox>
<el-checkbox label="waterQuality">水质</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="exportForm.format">
<el-radio label="excel">Excel</el-radio>
<el-radio label="csv">CSV</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="exportDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleExportConfirm">确认导出</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@use "./styles/variables" as v;
.env-container {
.env-card {
background: #fff;
border-radius: 8px;
padding: 20px;
height: 140px;
position: relative;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
.env-chart {
opacity: 1;
}
}
.env-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.label {
font-size: 14px;
color: $text-secondary;
}
.el-tag {
border: none;
padding: 2px 8px;
font-size: 12px;
height: 22px;
line-height: 20px;
border-radius: 4px;
}
}
.env-value {
font-size: 32px;
font-weight: 600;
color: $text-primary;
line-height: 1;
margin-bottom: 12px;
position: relative;
z-index: 1;
}
.env-chart {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50px;
opacity: 0.8;
transition: opacity 0.3s;
}
}
.el-card {
background: #ffffff;
border: none;
border-radius: 8px;
transition: all 0.3s;
box-shadow: $box-shadow;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08);
}
.el-card__header {
padding: 16px 20px;
border-bottom: 1px solid $border-color;
}
.el-tag {
transition: all 0.3s;
&.el-tag--success {
background-color: rgba(103, 194, 58, 0.1);
border-color: rgba(103, 194, 58, 0.2);
}
&.el-tag--warning {
background-color: rgba(230, 162, 60, 0.1);
border-color: rgba(230, 162, 60, 0.2);
}
}
}
.el-table {
:deep(tbody tr) {
transition: all 0.3s;
&:hover {
background-color: rgba(64, 158, 255, 0.1) !important;
}
}
}
:deep(.el-checkbox-group) {
display: flex;
gap: 16px;
.el-checkbox {
margin-right: 0;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
position: relative;
padding-left: 12px;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: $primary-color;
border-radius: 2px;
}
}
}
.mt-20 {
margin-top: 20px;
}
:deep(.el-table) {
th.el-table__cell {
background-color: #fafafa;
color: $text-primary;
font-weight: 500;
}
.el-table__cell {
padding: 12px 0;
}
}
.dialog-footer {
.el-button {
margin-left: 12px;
}
}
.el-card__body {
padding: 20px;
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
.el-dialog__header {
margin: 0;
padding: 20px;
border-bottom: 1px solid $border-color;
}
.el-dialog__body {
padding: 24px;
}
.el-dialog__footer {
padding: 16px 20px;
border-top: 1px solid $border-color;
}
}
.mb-20 {
margin-bottom: 20px;
}
}
</style>