654 lines
18 KiB
Vue

<script setup>
import { ref, onUnmounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import * as echarts from "echarts";
import { Plus } from "@element-plus/icons-vue";
// 模拟设备数据
const tableData = ref([
{
id: 1,
name: "A区-水质监测器-01",
type: "水质监测",
location: "A区湿地",
status: "在线",
ip: "192.168.1.101",
lastHeartbeat: "2024-03-20 12:00:00",
lastMaintenance: "2024-03-15",
},
{
id: 2,
name: "B区-空气监测器-01",
type: "空气监测",
location: "B区湿地",
status: "离线",
ip: "192.168.1.102",
lastHeartbeat: "2024-03-20 10:30:00",
lastMaintenance: "2024-03-10",
},
]);
// 设备类型选项
const deviceTypes = [
{ label: "水质监测", value: "水质监测" },
{ label: "空气监测", value: "空气监测" },
{ label: "视频监控", value: "视频监控" },
{ label: "气象站", value: "气象站" },
];
// 新增/编辑设备
const dialogVisible = ref(false);
const isEdit = ref(false);
const currentDevice = ref(null);
const formData = ref({
name: "",
type: "",
location: "",
ip: "",
});
// 表单验证规则
const rules = {
name: [{ required: true, message: "请输入设备名称", trigger: "blur" }],
type: [{ required: true, message: "请选择设备类型", trigger: "change" }],
location: [{ required: true, message: "请输入安装位置", trigger: "blur" }],
ip: [{ required: true, message: "请输入IP地址", trigger: "blur" }],
};
const formRef = ref();
// 打开新增弹窗
const handleAdd = () => {
isEdit.value = false;
currentDevice.value = null;
resetForm();
dialogVisible.value = true;
};
// 打开编辑弹窗
const handleEdit = (row) => {
isEdit.value = true;
currentDevice.value = row;
formData.value = {
name: row.name,
type: row.type,
location: row.location,
ip: row.ip,
};
dialogVisible.value = true;
};
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
if (isEdit.value && currentDevice.value) {
// 编辑模式
const index = tableData.value.findIndex(
(item) => item.id === currentDevice.value.id
);
if (index !== -1) {
tableData.value[index] = {
...currentDevice.value,
...formData.value,
lastHeartbeat: new Date().toLocaleString(),
};
ElMessage.success("更新成功");
}
} else {
// 新增模式
const newDevice = {
id: tableData.value.length + 1,
...formData.value,
status: "在线",
lastHeartbeat: new Date().toLocaleString(),
lastMaintenance: new Date().toLocaleDateString(),
};
tableData.value.push(newDevice);
ElMessage.success("添加成功");
}
dialogVisible.value = false;
} catch (error) {
console.error("表单验证失败:", error);
}
};
// 重置表单
const resetForm = () => {
formData.value = {
name: "",
type: "",
location: "",
ip: "",
};
if (formRef.value) {
formRef.value.resetFields();
}
};
// 删除设备
const handleDelete = (row) => {
ElMessageBox.confirm("确认删除该设备?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
ElMessage.success("删除成功");
}
})
.catch(() => {});
};
// 远程配置
const handleConfig = (row) => {
ElMessage.success("正在连接设备...");
};
// 设备维护
const handleMaintenance = (row) => {
ElMessage.success("已记录维护时间");
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value[index].lastMaintenance = new Date().toLocaleDateString();
}
};
// 搜索表单
const searchForm = ref({
name: "",
type: "",
status: "",
});
// 设备分组数据
const groupData = ref([
{
id: 1,
label: "A区设备",
children: [
{ id: 11, label: "水质监测设备" },
{ id: 12, label: "空气监测设备" },
],
},
{
id: 2,
label: "B区设备",
children: [
{ id: 21, label: "视频监控设备" },
{ id: 22, label: "气象站设备" },
],
},
]);
// 树形配置
const defaultProps = {
children: "children",
label: "label",
};
// 监控相关
const monitorVisible = ref(false);
const currentMonitorDevice = ref(null);
const monitorChart = ref(null);
// 模拟实时数据
const realTimeData = ref({
temperature: [],
humidity: [],
ph: [],
oxygen: [],
});
// 告警配置
const alarmConfig = ref({
offlineAlarm: true,
thresholds: {
temperature: { min: 0, max: 40 },
humidity: { min: 20, max: 80 },
ph: { min: 6.5, max: 8.5 },
oxygen: { min: 5, max: 9 },
},
});
// 搜索处理
const handleSearch = () => {
// 这里模拟搜索过滤
console.log("搜索条件:", searchForm.value);
};
const resetSearch = () => {
searchForm.value = {
name: "",
type: "",
status: "",
};
};
// 分组处理
const handleAddGroup = () => {
ElMessageBox.prompt("请输入分组名称", "新增分组", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(({ value }) => {
groupData.value.push({
id: Date.now(),
label: value,
children: [],
});
ElMessage.success("添加成功");
});
};
const handleNodeClick = (data) => {
console.log("选中分组:", data);
// 这里可以根据分组筛选设备列表
};
// 监控处理
const handleMonitor = (device) => {
currentMonitorDevice.value = device;
monitorVisible.value = true;
initMonitorChart();
startRealTimeData();
};
// 初始化监控图表
const initMonitorChart = () => {
const chartDom = document.getElementById("monitorChart");
if (!chartDom) return;
monitorChart.value = echarts.init(chartDom);
const option = {
title: {
text: "实时数据监控",
left: "center",
},
tooltip: {
trigger: "axis",
},
legend: {
data: ["温度", "湿度", "pH值", "溶解氧"],
top: 30,
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: Array.from({ length: 20 }, (_, i) => i.toString()),
},
yAxis: {
type: "value",
},
series: [
{
name: "温度",
type: "line",
data: [],
},
{
name: "湿度",
type: "line",
data: [],
},
{
name: "pH值",
type: "line",
data: [],
},
{
name: "溶解氧",
type: "line",
data: [],
},
],
};
monitorChart.value.setOption(option);
};
// 模拟实时数据更新
let dataTimer;
const startRealTimeData = () => {
clearInterval(dataTimer);
dataTimer = setInterval(() => {
// 模拟数据
const newData = {
temperature: Math.random() * 30 + 10,
humidity: Math.random() * 40 + 40,
ph: Math.random() * 2 + 6.5,
oxygen: Math.random() * 4 + 5,
};
// 检查告警
checkAlarms(newData);
// 更新图表
if (monitorChart.value) {
monitorChart.value.setOption({
series: [
{
data: [...realTimeData.value.temperature.slice(-19), newData.temperature],
},
{
data: [...realTimeData.value.humidity.slice(-19), newData.humidity],
},
{
data: [...realTimeData.value.ph.slice(-19), newData.ph],
},
{
data: [...realTimeData.value.oxygen.slice(-19), newData.oxygen],
},
],
});
}
// 更新数据记录
realTimeData.value = {
temperature: [...realTimeData.value.temperature.slice(-19), newData.temperature],
humidity: [...realTimeData.value.humidity.slice(-19), newData.humidity],
ph: [...realTimeData.value.ph.slice(-19), newData.ph],
oxygen: [...realTimeData.value.oxygen.slice(-19), newData.oxygen],
};
}, 1000);
};
// 检查告警
const checkAlarms = (data) => {
const { thresholds } = alarmConfig.value;
if (
data.temperature < thresholds.temperature.min ||
data.temperature > thresholds.temperature.max
) {
ElMessage.warning(`温度异常: ${data.temperature}°C`);
}
if (data.ph < thresholds.ph.min || data.ph > thresholds.ph.max) {
ElMessage.warning(`pH值异常: ${data.ph}`);
}
// ... 其他告警检查
};
// 组件卸载时清理定时器
onUnmounted(() => {
clearInterval(dataTimer);
});
</script>
<template>
<div class="device-container">
<!-- 设备分组和列表布局 -->
<el-row :gutter="20">
<!-- 左侧设备分组 -->
<el-col :span="4">
<el-card class="group-card">
<template #header>
<div class="card-header">
<span>设备分组</span>
<el-button type="text" @click="handleAddGroup">
<el-icon><Plus /></el-icon>
</el-button>
</div>
</template>
<el-tree
:data="groupData"
:props="defaultProps"
@node-click="handleNodeClick"
default-expand-all
/>
</el-card>
</el-col>
<!-- 右侧设备列表 -->
<el-col :span="20">
<el-card>
<template #header>
<div class="card-header">
<span>设备管理</span>
<el-button type="primary" @click="handleAdd">添加设备</el-button>
</div>
</template>
<!-- 搜索表单 -->
<el-form :model="searchForm" inline class="search-form">
<el-form-item label="设备名称">
<el-input v-model="searchForm.name" placeholder="请输入设备名称" clearable />
</el-form-item>
<el-form-item label="设备类型">
<el-select v-model="searchForm.type" placeholder="请选择" clearable>
<el-option
v-for="item in deviceTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-option label="在线" value="在线" />
<el-option label="离线" value="离线" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 设备列表 -->
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="设备名称" min-width="150" />
<el-table-column prop="type" label="设备类型" width="120" />
<el-table-column prop="location" label="安装位置" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '在线' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ip" label="IP地址" width="140" />
<el-table-column prop="lastHeartbeat" label="最后心跳" width="180" />
<el-table-column prop="lastMaintenance" label="最后维护" width="120" />
<el-table-column label="操作" width="380" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button type="success" size="small" @click="handleConfig(row)">
远程配置
</el-button>
<el-button type="info" size="small" @click="handleMonitor(row)">
实时监控
</el-button>
<el-button type="warning" size="small" @click="handleMaintenance(row)">
维护
</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑设备弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑设备' : '添加设备'"
width="600px"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="设备名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="设备类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择设备类型">
<el-option
v-for="item in deviceTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="安装位置" prop="location">
<el-input v-model="formData.location" placeholder="请输入安装位置" />
</el-form-item>
<el-form-item label="IP地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入IP地址" />
</el-form-item>
<el-form-item label="告警配置">
<el-collapse>
<el-collapse-item title="阈值设置">
<el-form :model="alarmConfig">
<el-form-item label="离线告警">
<el-switch v-model="alarmConfig.offlineAlarm" />
</el-form-item>
<el-form-item label="温度范围">
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.temperature.min"
:precision="1"
/>
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.temperature.max"
:precision="1"
/>
</el-col>
</el-form-item>
<el-form-item label="pH值范围">
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.ph.min"
:precision="1"
:step="0.1"
/>
</el-col>
<el-col :span="2" class="text-center">-</el-col>
<el-col :span="11">
<el-input-number
v-model="alarmConfig.thresholds.ph.max"
:precision="1"
:step="0.1"
/>
</el-col>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 实时监控弹窗 -->
<el-dialog v-model="monitorVisible" title="设备实时监控" width="800px" destroy-on-close>
<div class="monitor-container">
<el-row :gutter="20">
<el-col :span="8">
<div class="data-card">
<h3>实时数据</h3>
<el-descriptions :column="1" border>
<el-descriptions-item label="设备名称">
{{ currentMonitorDevice?.name }}
</el-descriptions-item>
<el-descriptions-item label="设备状态">
<el-tag
:type="currentMonitorDevice?.status === '在线' ? 'success' : 'danger'"
>
{{ currentMonitorDevice?.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最后心跳">
{{ currentMonitorDevice?.lastHeartbeat }}
</el-descriptions-item>
</el-descriptions>
</div>
</el-col>
<el-col :span="16">
<div id="monitorChart" style="height: 300px" />
</el-col>
</el-row>
</div>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@use "../../../styles/variables.scss" as *;
.device-container {
.group-card {
height: calc(100vh - 140px);
overflow-y: auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 16px;
font-weight: 500;
color: $text-primary;
}
}
.search-form {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid $border-color;
}
.text-center {
text-align: center;
line-height: 32px;
}
.monitor-container {
.data-card {
padding: 20px;
background: #f8f9fa;
border-radius: 4px;
h3 {
margin: 0 0 16px;
font-size: 16px;
color: $text-primary;
}
}
}
:deep(.el-card) {
border: none;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}
</style>