678 lines
15 KiB
Vue
678 lines
15 KiB
Vue
<script setup lang="ts">
|
|
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";
|
|
|
|
interface EnvData {
|
|
id: number;
|
|
location: string;
|
|
temperature: number;
|
|
humidity: number;
|
|
waterQuality: string;
|
|
time: string;
|
|
}
|
|
|
|
const tableData = ref<EnvData[]>([
|
|
{
|
|
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: any) {
|
|
let result = `${params[0].axisValue}<br/>`;
|
|
params.forEach((item: any) => {
|
|
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: [] as string[],
|
|
format: "excel",
|
|
dataType: ["temperature", "humidity", "waterQuality"],
|
|
});
|
|
|
|
const handleExport = () => {
|
|
exportDialogVisible.value = true;
|
|
};
|
|
|
|
const handleExportConfirm = () => {
|
|
console.log("导出数据:", exportForm);
|
|
exportDialogVisible.value = false;
|
|
};
|
|
|
|
// 初始化迷你图表
|
|
const initMiniChart = (el: HTMLElement, data: number[]) => {
|
|
const chart = echarts.init(el);
|
|
chart.setOption({
|
|
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],
|
|
},
|
|
});
|
|
|
|
// 监听卡片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(() => {
|
|
// 初始化所有迷你图表
|
|
envStats.value.forEach((stat, index) => {
|
|
const el = document.getElementById(`miniChart${index}`);
|
|
if (el) {
|
|
initMiniChart(el, stat.trend);
|
|
}
|
|
});
|
|
// 原有的图表初始化
|
|
initChart();
|
|
});
|
|
</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>
|
|
@import "../../../styles/variables.scss";
|
|
|
|
.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>
|