654 lines
18 KiB
Vue
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>
|