|
@@ -0,0 +1,407 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>新风机组监控</title>
|
|
|
+ <script src="/static/css/tailwindcss.css"></script>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ tailwind.config = {
|
|
|
+ theme: {
|
|
|
+ extend: {
|
|
|
+ colors: {
|
|
|
+ primary: '#0f766e',
|
|
|
+ secondary: '#0d9488',
|
|
|
+ accent: '#14b8a6',
|
|
|
+ neutral: '#134e4a',
|
|
|
+ },
|
|
|
+ fontFamily: {
|
|
|
+ sans: ['Inter', 'system-ui', 'sans-serif'],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+
|
|
|
+ <style type="text/tailwindcss">
|
|
|
+ @layer utilities {
|
|
|
+ .card-shadow {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ }
|
|
|
+ .status-normal {
|
|
|
+ @apply text-emerald-500;
|
|
|
+ }
|
|
|
+ .status-warning {
|
|
|
+ @apply text-amber-500;
|
|
|
+ }
|
|
|
+ .status-alarm {
|
|
|
+ @apply text-rose-500;
|
|
|
+ }
|
|
|
+ .data-update {
|
|
|
+ animation: pulse 1s ease-in-out;
|
|
|
+ }
|
|
|
+ @keyframes pulse {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.6; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+
|
|
|
+<body class="bg-gradient-to-br from-primary to-neutral text-gray-800 min-h-screen p-3">
|
|
|
+<div class="container mx-auto max-w-7xl">
|
|
|
+ <!-- 头部信息 - 更紧凑 -->
|
|
|
+ <header class="mb-4">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <h1 class="text-[clamp(1.2rem,2vw,1.6rem)] font-bold text-white">{{.deviceName}}</h1>
|
|
|
+ <div class="flex items-center bg-white/10 px-2 py-1 rounded-lg">
|
|
|
+ <i class="fa fa-refresh mr-1 text-white text-sm"></i>
|
|
|
+ <span id="connection-status" class="text-white text-xs">连接中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="h-1 w-full bg-gradient-to-r from-accent to-transparent rounded-full mt-1"></div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- 流程图区域 - 调整大小 -->
|
|
|
+ <div class="bg-white/90 rounded-lg p-4 mb-4 card-shadow">
|
|
|
+ <h2 class="text-base font-semibold mb-2 flex items-center">
|
|
|
+ <i class="fa fa-sitemap text-primary mr-1"></i>系统流程图
|
|
|
+ </h2>
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+<!-- <div class="bg-blue-100 px-2 py-1 rounded text-primary font-medium text-sm">-->
|
|
|
+<!-- <i class="fa fa-arrow-right mr-1"></i> 新风-->
|
|
|
+<!-- </div>-->
|
|
|
+ <img src="/static/images/PAU.png" alt="新风机组流程图" class="max-w-[80%] h-auto rounded shadow-sm">
|
|
|
+<!-- <div class="bg-green-100 px-2 py-1 rounded text-green-600 font-medium text-sm">-->
|
|
|
+<!-- 送风 <i class="fa fa-arrow-right ml-1"></i>-->
|
|
|
+<!-- </div>-->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主要内容区域 - 紧凑布局 -->
|
|
|
+ <div id="main-content" class="space-y-4">
|
|
|
+ <!-- 数据卡片区域 - 更密的网格 -->
|
|
|
+ <div id="data-cards" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
|
|
|
+ <!-- 数据卡片将在这里动态生成 -->
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 状态和控制参数合并区域 - 节省空间 -->
|
|
|
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
+ <!-- 状态参数区域 -->
|
|
|
+ <div id="status-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
|
|
|
+ <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
|
|
|
+ <i class="fa fa-tachometer mr-1"></i>状态参数
|
|
|
+ </h2>
|
|
|
+ <div id="status-parameters" class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
|
+ <!-- 状态参数将在这里动态生成 -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 控制参数区域 -->
|
|
|
+ <div id="control-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
|
|
|
+ <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
|
|
|
+ <i class="fa fa-sliders mr-1"></i>控制参数
|
|
|
+ </h2>
|
|
|
+ <div id="control-parameters" class="space-y-2">
|
|
|
+ <!-- 控制参数将在这里动态生成 -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据更新日志 - 缩小高度 -->
|
|
|
+ <div id="log-section" class="bg-white/90 rounded-lg p-3 card-shadow">
|
|
|
+ <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
|
|
|
+ <i class="fa fa-history mr-1"></i>数据更新日志
|
|
|
+ </h2>
|
|
|
+ <div id="update-log" class="text-xs text-gray-600 max-h-20 overflow-y-auto space-y-0.5">
|
|
|
+ <p class="text-gray-500">等待数据更新...</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+ // 数据类型配置 - 定义不同字段的显示和交互方式
|
|
|
+ const fieldConfig = {
|
|
|
+ // 温度类字段
|
|
|
+ "新风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "blue" },
|
|
|
+ "送风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "green" },
|
|
|
+ "回风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "purple" },
|
|
|
+
|
|
|
+ // 湿度类字段
|
|
|
+ "送风湿度": { type: "humidity", unit: "%", icon: "fa-tint", color: "blue" },
|
|
|
+
|
|
|
+ // 二氧化碳
|
|
|
+ "回风二氧化碳": { type: "co2", unit: "ppm", icon: "fa-leaf", color: "green" },
|
|
|
+
|
|
|
+ // 阀门反馈
|
|
|
+ "回风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "gray" },
|
|
|
+ "新风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "blue" },
|
|
|
+ "水阀反馈": { type: "valve", unit: "%", icon: "fa-tint", color: "cyan" },
|
|
|
+
|
|
|
+ // 模式类(开关)
|
|
|
+ "冬夏季模式": { type: "mode", trueText: "夏季模式", falseText: "冬季模式", section: "control" },
|
|
|
+
|
|
|
+ // 状态类
|
|
|
+ "运行状态": { type: "status", trueText: "运行中", falseText: "停止", trueClass: "status-normal", falseClass: "status-warning" },
|
|
|
+ "故障报警": { type: "status", trueText: "故障", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
|
|
|
+ "自动状态": { type: "status", trueText: "自动", falseText: "手动", trueClass: "status-normal", falseClass: "status-warning" },
|
|
|
+ "过滤网压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
|
|
|
+ "防冻开关": { type: "status", trueText: "触发", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
|
|
|
+ "风机压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 连接SSE并处理数据
|
|
|
+ function connectSSE() {
|
|
|
+ const statusElement = document.getElementById('connection-status');
|
|
|
+ const logElement = document.getElementById('update-log');
|
|
|
+ const dataCardsContainer = document.getElementById('data-cards');
|
|
|
+ const statusParametersContainer = document.getElementById('status-parameters');
|
|
|
+ const controlParametersContainer = document.getElementById('control-parameters');
|
|
|
+ const statusSection = document.getElementById('status-section');
|
|
|
+ const controlSection = document.getElementById('control-section');
|
|
|
+
|
|
|
+ // 跟踪已创建的元素,避免重复创建
|
|
|
+ const createdElements = new Set();
|
|
|
+
|
|
|
+ // 更新连接状态
|
|
|
+ function updateConnectionStatus(status, isConnected) {
|
|
|
+ statusElement.textContent = status;
|
|
|
+ if (isConnected) {
|
|
|
+ statusElement.classList.remove('text-amber-400', 'text-rose-400');
|
|
|
+ statusElement.classList.add('text-emerald-400');
|
|
|
+ } else if (status.includes('连接中')) {
|
|
|
+ statusElement.classList.remove('text-emerald-400', 'text-rose-400');
|
|
|
+ statusElement.classList.add('text-amber-400');
|
|
|
+ } else {
|
|
|
+ statusElement.classList.remove('text-emerald-400', 'text-amber-400');
|
|
|
+ statusElement.classList.add('text-rose-400');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加日志记录
|
|
|
+ function addLogEntry(message) {
|
|
|
+ const now = new Date();
|
|
|
+ const timeString = now.toLocaleTimeString();
|
|
|
+ const logEntry = document.createElement('p');
|
|
|
+ logEntry.innerHTML = `<span class="text-primary">[${timeString}]</span> ${message}`;
|
|
|
+
|
|
|
+ // 移除"等待数据更新..."提示
|
|
|
+ if (logElement.querySelector('.text-gray-500')) {
|
|
|
+ logElement.innerHTML = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ logElement.appendChild(logEntry);
|
|
|
+ logElement.scrollTop = logElement.scrollHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建或更新数据卡片 - 更紧凑的卡片
|
|
|
+ function createOrUpdateDataCard(field, value) {
|
|
|
+ const config = fieldConfig[field] || { type: "generic", unit: "", icon: "fa-dashboard", color: "gray" };
|
|
|
+ const elementId = `card-${field.replace(/\s+/g, '-')}`;
|
|
|
+
|
|
|
+ // 如果元素已存在,只更新值
|
|
|
+ if (createdElements.has(elementId)) {
|
|
|
+ const valueElement = document.getElementById(`${elementId}-value`);
|
|
|
+ if (valueElement) {
|
|
|
+ valueElement.textContent = `${value}${config.unit}`;
|
|
|
+ valueElement.classList.add('data-update');
|
|
|
+ setTimeout(() => valueElement.classList.remove('data-update'), 1000);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的数据卡片
|
|
|
+ const card = document.createElement('div');
|
|
|
+ card.id = elementId;
|
|
|
+ card.className = "bg-white/90 rounded-lg p-2 card-shadow transform transition-all hover:scale-[1.02]";
|
|
|
+
|
|
|
+ // 确定图标颜色类
|
|
|
+ const iconColorClass = config.color === 'blue' ? 'text-blue-600' :
|
|
|
+ config.color === 'green' ? 'text-green-600' :
|
|
|
+ config.color === 'purple' ? 'text-purple-600' :
|
|
|
+ config.color === 'cyan' ? 'text-cyan-600' :
|
|
|
+ 'text-gray-600';
|
|
|
+
|
|
|
+ const bgColorClass = config.color === 'blue' ? 'bg-blue-100' :
|
|
|
+ config.color === 'green' ? 'bg-green-100' :
|
|
|
+ config.color === 'purple' ? 'bg-purple-100' :
|
|
|
+ config.color === 'cyan' ? 'bg-cyan-100' :
|
|
|
+ 'bg-gray-100';
|
|
|
+
|
|
|
+ card.innerHTML = `
|
|
|
+ <div class="flex justify-between items-start">
|
|
|
+ <div>
|
|
|
+ <p class="text-gray-500 text-xs">${field}</p>
|
|
|
+ <h3 id="${elementId}-value" class="text-lg font-bold mt-0.5">${value}${config.unit}</h3>
|
|
|
+ </div>
|
|
|
+ <div class="${bgColorClass} p-1.5 rounded-full">
|
|
|
+ <i class="fa ${config.icon} ${iconColorClass} text-sm"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ dataCardsContainer.appendChild(card);
|
|
|
+ createdElements.add(elementId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建或更新状态参数 - 更紧凑的参数项
|
|
|
+ function createOrUpdateStatusParameter(field, value) {
|
|
|
+ const config = fieldConfig[field] || { type: "generic", trueText: "是", falseText: "否" };
|
|
|
+ const elementId = `status-${field.replace(/\s+/g, '-')}`;
|
|
|
+
|
|
|
+ // 确定应该显示在哪个区域
|
|
|
+ const targetContainer = config.section === "control" ? controlParametersContainer : statusParametersContainer;
|
|
|
+ const targetSection = config.section === "control" ? controlSection : statusSection;
|
|
|
+
|
|
|
+ // 显示对应的区域
|
|
|
+ targetSection.classList.remove('hidden');
|
|
|
+
|
|
|
+ // 如果元素已存在,只更新值
|
|
|
+ if (createdElements.has(elementId)) {
|
|
|
+ const valueElement = document.getElementById(`${elementId}-value`);
|
|
|
+ if (valueElement) {
|
|
|
+ if (config.type === "status") {
|
|
|
+ const isTrue = value === "是";
|
|
|
+ const displayValue = config.invert ? !isTrue : isTrue;
|
|
|
+ const text = displayValue ? config.trueText : config.falseText;
|
|
|
+
|
|
|
+ valueElement.textContent = text;
|
|
|
+ valueElement.className = `px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}`;
|
|
|
+ } else if (config.type === "mode") {
|
|
|
+ const isChecked = value === "是";
|
|
|
+ valueElement.checked = isChecked;
|
|
|
+ } else {
|
|
|
+ valueElement.textContent = value;
|
|
|
+ }
|
|
|
+
|
|
|
+ valueElement.classList.add('data-update');
|
|
|
+ setTimeout(() => valueElement.classList.remove('data-update'), 1000);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的参数元素
|
|
|
+ const parameter = document.createElement('div');
|
|
|
+ parameter.id = elementId;
|
|
|
+
|
|
|
+ if (config.type === "status") {
|
|
|
+ const isTrue = value === "是";
|
|
|
+ const displayValue = config.invert ? !isTrue : isTrue;
|
|
|
+ const text = displayValue ? config.trueText : config.falseText;
|
|
|
+
|
|
|
+ parameter.className = config.section === "control"
|
|
|
+ ? "flex justify-between items-center border-b border-gray-100 pb-2"
|
|
|
+ : "bg-gray-50 p-2.5 rounded text-sm";
|
|
|
+
|
|
|
+ parameter.innerHTML = `
|
|
|
+ <label class="font-medium text-sm">${field}</label>
|
|
|
+ <span id="${elementId}-value" class="px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}">
|
|
|
+ ${text}
|
|
|
+ </span>
|
|
|
+ `;
|
|
|
+ } else if (config.type === "mode") {
|
|
|
+ const isChecked = value === "是";
|
|
|
+
|
|
|
+ parameter.className = "flex justify-between items-center border-b border-gray-100 pb-2";
|
|
|
+ parameter.innerHTML = `
|
|
|
+ <label class="font-medium text-sm">${field}</label>
|
|
|
+ <label class="inline-flex items-center cursor-pointer">
|
|
|
+ <input type="checkbox" id="${elementId}-value" ${isChecked ? 'checked' : ''} class="sr-only peer">
|
|
|
+ <div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[1px] after:left-[1px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
|
|
|
+ <span class="ml-2 text-xs font-medium">${isChecked ? config.trueText : config.falseText}</span>
|
|
|
+ </label>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 添加模式切换事件
|
|
|
+ setTimeout(() => {
|
|
|
+ const checkbox = document.getElementById(`${elementId}-value`);
|
|
|
+ if (checkbox) {
|
|
|
+ checkbox.addEventListener('change', function() {
|
|
|
+ const newValue = this.checked ? "是" : "否";
|
|
|
+ const displayText = this.checked ? config.trueText : config.falseText;
|
|
|
+ this.nextElementSibling.nextElementSibling.textContent = displayText;
|
|
|
+
|
|
|
+ addLogEntry(`${field}已切换为${displayText}`);
|
|
|
+ // 这里可以添加发送到服务器的逻辑
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, 0);
|
|
|
+ } else {
|
|
|
+ parameter.className = config.section === "control"
|
|
|
+ ? "flex justify-between items-center border-b border-gray-100 pb-2"
|
|
|
+ : "bg-gray-50 p-2.5 rounded text-sm";
|
|
|
+
|
|
|
+ parameter.innerHTML = `
|
|
|
+ <label class="font-medium text-sm">${field}</label>
|
|
|
+ <span id="${elementId}-value" class="text-base font-semibold">${value}</span>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ targetContainer.appendChild(parameter);
|
|
|
+ createdElements.add(elementId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断字段应该显示的位置
|
|
|
+ function isStatusOrControlField(field) {
|
|
|
+ const config = fieldConfig[field];
|
|
|
+ return config && (config.type === "status" || config.type === "mode");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理接收到的SSE数据
|
|
|
+ function handleSSEData(data) {
|
|
|
+ addLogEntry(`接收${Object.keys(data).length}个字段数据`);
|
|
|
+
|
|
|
+ // 处理每个返回的字段
|
|
|
+ Object.keys(data).forEach(field => {
|
|
|
+ const value = data[field];
|
|
|
+
|
|
|
+ // 根据字段类型决定显示方式和位置
|
|
|
+ if (isStatusOrControlField(field)) {
|
|
|
+ createOrUpdateStatusParameter(field, value);
|
|
|
+ } else {
|
|
|
+ createOrUpdateDataCard(field, value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化连接状态
|
|
|
+ updateConnectionStatus('连接中...', false);
|
|
|
+ let urls ="./pointSSE?deviceName="+{{.deviceName}}
|
|
|
+ // 实际SSE连接代码
|
|
|
+ console.log("url地址===========>",urls)
|
|
|
+ const eventSource = new EventSource(urls);
|
|
|
+
|
|
|
+ eventSource.onopen = () => {
|
|
|
+ updateConnectionStatus('已连接', true);
|
|
|
+ addLogEntry('SSE连接已建立');
|
|
|
+ };
|
|
|
+
|
|
|
+ eventSource.onmessage = (event) => {
|
|
|
+ try {
|
|
|
+ const data = JSON.parse(event.data);
|
|
|
+ if (data.code === 200 && data.data) {
|
|
|
+ handleSSEData(data.data);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ addLogEntry(`数据解析错误: ${error.message}`);
|
|
|
+ console.error('SSE数据解析错误:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ eventSource.onerror = (error) => {
|
|
|
+ updateConnectionStatus('连接错误', false);
|
|
|
+ addLogEntry('SSE连接发生错误,正在重试...');
|
|
|
+ console.error('SSE错误:', error);
|
|
|
+ };
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 页面加载完成后连接SSE
|
|
|
+ document.addEventListener('DOMContentLoaded', connectSSE);
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+
|
|
|
+</html>
|