JSK.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>集水坑监控</title>
  7. <script src="/static/css/tailwindcss.css"></script>
  8. <script>
  9. tailwind.config = {
  10. theme: {
  11. extend: {
  12. colors: {
  13. primary: '#0f766e',
  14. secondary: '#0d9488',
  15. accent: '#14b8a6',
  16. neutral: '#134e4a',
  17. },
  18. fontFamily: {
  19. sans: ['Inter', 'system-ui', 'sans-serif'],
  20. },
  21. },
  22. }
  23. }
  24. </script>
  25. <style type="text/tailwindcss">
  26. @layer utilities {
  27. .card-shadow {
  28. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  29. }
  30. .status-normal {
  31. @apply text-emerald-500;
  32. }
  33. .status-warning {
  34. @apply text-amber-500;
  35. }
  36. .status-alarm {
  37. @apply text-rose-500;
  38. }
  39. .data-update {
  40. animation: pulse 1s ease-in-out;
  41. }
  42. @keyframes pulse {
  43. 0%, 100% { opacity: 1; }
  44. 50% { opacity: 0.6; }
  45. }
  46. }
  47. </style>
  48. </head>
  49. <body class="bg-gradient-to-br from-primary to-neutral text-gray-800 min-h-screen p-3">
  50. <div class="container mx-auto max-w-7xl">
  51. <!-- 头部信息 - 更紧凑 -->
  52. <header class="mb-4">
  53. <div class="flex justify-between items-center">
  54. <h1 class="text-[clamp(1.2rem,2vw,1.6rem)] font-bold text-white">{{.deviceName}}</h1>
  55. <div class="flex items-center bg-white/10 px-2 py-1 rounded-lg">
  56. <i class="fa fa-refresh mr-1 text-white text-sm"></i>
  57. <span id="connection-status" class="text-white text-xs">连接中...</span>
  58. </div>
  59. </div>
  60. <div class="h-1 w-full bg-gradient-to-r from-accent to-transparent rounded-full mt-1"></div>
  61. </header>
  62. <!-- 流程图区域 - 调整大小 -->
  63. <div class="bg-white/90 rounded-lg p-4 mb-4 card-shadow">
  64. <h2 class="text-base font-semibold mb-2 flex items-center">
  65. <i class="fa fa-sitemap text-primary mr-1"></i>系统流程图
  66. </h2>
  67. <div class="flex items-center justify-between">
  68. <!-- <div class="bg-blue-100 px-2 py-1 rounded text-primary font-medium text-sm">-->
  69. <!-- <i class="fa fa-arrow-right mr-1"></i> 新风-->
  70. <!-- </div>-->
  71. <img src="/static/images/JSK.png" alt="新风机组流程图" class="max-w-[80%] h-auto rounded shadow-sm">
  72. <!-- <div class="bg-green-100 px-2 py-1 rounded text-green-600 font-medium text-sm">-->
  73. <!-- 送风 <i class="fa fa-arrow-right ml-1"></i>-->
  74. <!-- </div>-->
  75. </div>
  76. </div>
  77. <!-- 主要内容区域 - 紧凑布局 -->
  78. <div id="main-content" class="space-y-4">
  79. <!-- 数据卡片区域 - 更密的网格 -->
  80. <div id="data-cards" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
  81. <!-- 数据卡片将在这里动态生成 -->
  82. </div>
  83. <!-- 状态和控制参数合并区域 - 节省空间 -->
  84. <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
  85. <!-- 状态参数区域 -->
  86. <div id="status-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
  87. <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
  88. <i class="fa fa-tachometer mr-1"></i>状态参数
  89. </h2>
  90. <div id="status-parameters" class="grid grid-cols-1 sm:grid-cols-2 gap-2">
  91. <!-- 状态参数将在这里动态生成 -->
  92. </div>
  93. </div>
  94. <!-- 控制参数区域 -->
  95. <div id="control-section" class="bg-white/90 rounded-lg p-3 card-shadow hidden">
  96. <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
  97. <i class="fa fa-sliders mr-1"></i>控制参数
  98. </h2>
  99. <div id="control-parameters" class="space-y-2">
  100. <!-- 控制参数将在这里动态生成 -->
  101. </div>
  102. </div>
  103. </div>
  104. <!-- 数据更新日志 - 缩小高度 -->
  105. <div id="log-section" class="bg-white/90 rounded-lg p-3 card-shadow">
  106. <h2 class="text-base font-semibold mb-2 flex items-center text-primary">
  107. <i class="fa fa-history mr-1"></i>数据更新日志
  108. </h2>
  109. <div id="update-log" class="text-xs text-gray-600 max-h-20 overflow-y-auto space-y-0.5">
  110. <p class="text-gray-500">等待数据更新...</p>
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. <script>
  116. // 数据类型配置 - 定义不同字段的显示和交互方式
  117. const fieldConfig = {
  118. // 温度类字段
  119. "新风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "blue" },
  120. "送风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "green" },
  121. "回风温度": { type: "temperature", unit: "°C", icon: "fa-thermometer-half", color: "purple" },
  122. // 湿度类字段
  123. "送风湿度": { type: "humidity", unit: "%", icon: "fa-tint", color: "blue" },
  124. // 二氧化碳
  125. "回风二氧化碳": { type: "co2", unit: "ppm", icon: "fa-leaf", color: "green" },
  126. // 阀门反馈
  127. "回风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "gray" },
  128. "新风阀反馈": { type: "valve", unit: "%", icon: "fa-exchange", color: "blue" },
  129. "水阀反馈": { type: "valve", unit: "%", icon: "fa-tint", color: "cyan" },
  130. // 模式类(开关)
  131. "冬夏季模式": { type: "mode", trueText: "夏季模式", falseText: "冬季模式", section: "control" },
  132. // 状态类
  133. "运行状态": { type: "status", trueText: "运行中", falseText: "停止", trueClass: "status-normal", falseClass: "status-warning" },
  134. "故障报警": { type: "status", trueText: "故障", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
  135. "自动状态": { type: "status", trueText: "自动", falseText: "手动", trueClass: "status-normal", falseClass: "status-warning" },
  136. "过滤网压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
  137. "防冻开关": { type: "status", trueText: "触发", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true },
  138. "风机压差": { type: "status", trueText: "异常", falseText: "正常", trueClass: "status-alarm", falseClass: "status-normal", invert: true }
  139. };
  140. // 连接SSE并处理数据
  141. function connectSSE() {
  142. const statusElement = document.getElementById('connection-status');
  143. const logElement = document.getElementById('update-log');
  144. const dataCardsContainer = document.getElementById('data-cards');
  145. const statusParametersContainer = document.getElementById('status-parameters');
  146. const controlParametersContainer = document.getElementById('control-parameters');
  147. const statusSection = document.getElementById('status-section');
  148. const controlSection = document.getElementById('control-section');
  149. // 跟踪已创建的元素,避免重复创建
  150. const createdElements = new Set();
  151. // 更新连接状态
  152. function updateConnectionStatus(status, isConnected) {
  153. statusElement.textContent = status;
  154. if (isConnected) {
  155. statusElement.classList.remove('text-amber-400', 'text-rose-400');
  156. statusElement.classList.add('text-emerald-400');
  157. } else if (status.includes('连接中')) {
  158. statusElement.classList.remove('text-emerald-400', 'text-rose-400');
  159. statusElement.classList.add('text-amber-400');
  160. } else {
  161. statusElement.classList.remove('text-emerald-400', 'text-amber-400');
  162. statusElement.classList.add('text-rose-400');
  163. }
  164. }
  165. // 添加日志记录
  166. function addLogEntry(message) {
  167. const now = new Date();
  168. const timeString = now.toLocaleTimeString();
  169. const logEntry = document.createElement('p');
  170. logEntry.innerHTML = `<span class="text-primary">[${timeString}]</span> ${message}`;
  171. // 移除"等待数据更新..."提示
  172. if (logElement.querySelector('.text-gray-500')) {
  173. logElement.innerHTML = '';
  174. }
  175. logElement.appendChild(logEntry);
  176. logElement.scrollTop = logElement.scrollHeight;
  177. }
  178. // 创建或更新数据卡片 - 更紧凑的卡片
  179. function createOrUpdateDataCard(field, value) {
  180. const config = fieldConfig[field] || { type: "generic", unit: "", icon: "fa-dashboard", color: "gray" };
  181. const elementId = `card-${field.replace(/\s+/g, '-')}`;
  182. // 如果元素已存在,只更新值
  183. if (createdElements.has(elementId)) {
  184. const valueElement = document.getElementById(`${elementId}-value`);
  185. if (valueElement) {
  186. valueElement.textContent = `${value}${config.unit}`;
  187. valueElement.classList.add('data-update');
  188. setTimeout(() => valueElement.classList.remove('data-update'), 1000);
  189. }
  190. return;
  191. }
  192. // 创建新的数据卡片
  193. const card = document.createElement('div');
  194. card.id = elementId;
  195. card.className = "bg-white/90 rounded-lg p-2 card-shadow transform transition-all hover:scale-[1.02]";
  196. // 确定图标颜色类
  197. const iconColorClass = config.color === 'blue' ? 'text-blue-600' :
  198. config.color === 'green' ? 'text-green-600' :
  199. config.color === 'purple' ? 'text-purple-600' :
  200. config.color === 'cyan' ? 'text-cyan-600' :
  201. 'text-gray-600';
  202. const bgColorClass = config.color === 'blue' ? 'bg-blue-100' :
  203. config.color === 'green' ? 'bg-green-100' :
  204. config.color === 'purple' ? 'bg-purple-100' :
  205. config.color === 'cyan' ? 'bg-cyan-100' :
  206. 'bg-gray-100';
  207. card.innerHTML = `
  208. <div class="flex justify-between items-start">
  209. <div>
  210. <p class="text-gray-500 text-xs">${field}</p>
  211. <h3 id="${elementId}-value" class="text-lg font-bold mt-0.5">${value}${config.unit}</h3>
  212. </div>
  213. <div class="${bgColorClass} p-1.5 rounded-full">
  214. <i class="fa ${config.icon} ${iconColorClass} text-sm"></i>
  215. </div>
  216. </div>
  217. `;
  218. dataCardsContainer.appendChild(card);
  219. createdElements.add(elementId);
  220. }
  221. // 创建或更新状态参数 - 更紧凑的参数项
  222. function createOrUpdateStatusParameter(field, value) {
  223. const config = fieldConfig[field] || { type: "generic", trueText: "是", falseText: "否" };
  224. const elementId = `status-${field.replace(/\s+/g, '-')}`;
  225. // 确定应该显示在哪个区域
  226. const targetContainer = config.section === "control" ? controlParametersContainer : statusParametersContainer;
  227. const targetSection = config.section === "control" ? controlSection : statusSection;
  228. // 显示对应的区域
  229. targetSection.classList.remove('hidden');
  230. // 如果元素已存在,只更新值
  231. if (createdElements.has(elementId)) {
  232. const valueElement = document.getElementById(`${elementId}-value`);
  233. if (valueElement) {
  234. if (config.type === "status") {
  235. const isTrue = value === "是";
  236. const displayValue = config.invert ? !isTrue : isTrue;
  237. const text = displayValue ? config.trueText : config.falseText;
  238. valueElement.textContent = text;
  239. valueElement.className = `px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}`;
  240. } else if (config.type === "mode") {
  241. const isChecked = value === "是";
  242. valueElement.checked = isChecked;
  243. } else {
  244. valueElement.textContent = value;
  245. }
  246. valueElement.classList.add('data-update');
  247. setTimeout(() => valueElement.classList.remove('data-update'), 1000);
  248. }
  249. return;
  250. }
  251. // 创建新的参数元素
  252. const parameter = document.createElement('div');
  253. parameter.id = elementId;
  254. if (config.type === "status") {
  255. const isTrue = value === "是";
  256. const displayValue = config.invert ? !isTrue : isTrue;
  257. const text = displayValue ? config.trueText : config.falseText;
  258. parameter.className = config.section === "control"
  259. ? "flex justify-between items-center border-b border-gray-100 pb-2"
  260. : "bg-gray-50 p-2.5 rounded text-sm";
  261. parameter.innerHTML = `
  262. <label class="font-medium text-sm">${field}</label>
  263. <span id="${elementId}-value" class="px-2 py-0.5 rounded-full text-xs ${displayValue ? config.trueClass : config.falseClass}">
  264. ${text}
  265. </span>
  266. `;
  267. } else if (config.type === "mode") {
  268. const isChecked = value === "是";
  269. parameter.className = "flex justify-between items-center border-b border-gray-100 pb-2";
  270. parameter.innerHTML = `
  271. <label class="font-medium text-sm">${field}</label>
  272. <label class="inline-flex items-center cursor-pointer">
  273. <input type="checkbox" id="${elementId}-value" ${isChecked ? 'checked' : ''} class="sr-only peer">
  274. <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>
  275. <span class="ml-2 text-xs font-medium">${isChecked ? config.trueText : config.falseText}</span>
  276. </label>
  277. `;
  278. // 添加模式切换事件
  279. setTimeout(() => {
  280. const checkbox = document.getElementById(`${elementId}-value`);
  281. if (checkbox) {
  282. checkbox.addEventListener('change', function() {
  283. const newValue = this.checked ? "是" : "否";
  284. const displayText = this.checked ? config.trueText : config.falseText;
  285. this.nextElementSibling.nextElementSibling.textContent = displayText;
  286. addLogEntry(`${field}已切换为${displayText}`);
  287. // 这里可以添加发送到服务器的逻辑
  288. });
  289. }
  290. }, 0);
  291. } else {
  292. parameter.className = config.section === "control"
  293. ? "flex justify-between items-center border-b border-gray-100 pb-2"
  294. : "bg-gray-50 p-2.5 rounded text-sm";
  295. parameter.innerHTML = `
  296. <label class="font-medium text-sm">${field}</label>
  297. <span id="${elementId}-value" class="text-base font-semibold">${value}</span>
  298. `;
  299. }
  300. targetContainer.appendChild(parameter);
  301. createdElements.add(elementId);
  302. }
  303. // 判断字段应该显示的位置
  304. function isStatusOrControlField(field) {
  305. const config = fieldConfig[field];
  306. return config && (config.type === "status" || config.type === "mode");
  307. }
  308. // 处理接收到的SSE数据
  309. function handleSSEData(data) {
  310. addLogEntry(`接收${Object.keys(data).length}个字段数据`);
  311. // 处理每个返回的字段
  312. Object.keys(data).forEach(field => {
  313. const value = data[field];
  314. // 根据字段类型决定显示方式和位置
  315. if (isStatusOrControlField(field)) {
  316. createOrUpdateStatusParameter(field, value);
  317. } else {
  318. createOrUpdateDataCard(field, value);
  319. }
  320. });
  321. }
  322. // 初始化连接状态
  323. updateConnectionStatus('连接中...', false);
  324. let urls ="./pointSSE?deviceName="+{{.deviceName}}
  325. // 实际SSE连接代码
  326. const eventSource = new EventSource(urls);
  327. eventSource.onopen = () => {
  328. updateConnectionStatus('已连接', true);
  329. addLogEntry('SSE连接已建立');
  330. };
  331. eventSource.onmessage = (event) => {
  332. try {
  333. const data = JSON.parse(event.data);
  334. if (data.code === 200 && data.data) {
  335. handleSSEData(data.data);
  336. }
  337. } catch (error) {
  338. addLogEntry(`数据解析错误: ${error.message}`);
  339. console.error('SSE数据解析错误:', error);
  340. }
  341. };
  342. eventSource.onerror = (error) => {
  343. updateConnectionStatus('连接错误', false);
  344. addLogEntry('SSE连接发生错误,正在重试...');
  345. console.error('SSE错误:', error);
  346. };
  347. }
  348. // 页面加载完成后连接SSE
  349. document.addEventListener('DOMContentLoaded', connectSSE);
  350. </script>
  351. </body>
  352. </html>