Browse Source

新增电暖器统计

yangfei 1 year ago
parent
commit
e218c5b9fa
9 changed files with 831 additions and 162 deletions
  1. 268 4
      package-lock.json
  2. 6 3
      package.json
  3. 8 1
      src/App.vue
  4. 105 0
      src/components/HeaterUsage.vue
  5. 386 123
      src/components/OTAUpgrade.vue
  6. 18 18
      src/components/RoomManagement.vue
  7. 16 6
      src/main.js
  8. 3 0
      src/router.js
  9. 21 7
      src/router/index.js

+ 268 - 4
package-lock.json

@@ -1,21 +1,25 @@
 {
-  "name": "relay-control",
+  "name": "relay-control1",
   "version": "0.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
-      "name": "relay-control",
+      "name": "relay-control1",
       "version": "0.1.0",
       "dependencies": {
+        "@element-plus/icons-vue": "^2.3.1",
         "axios": "^1.6.2",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "cors": "^2.8.5",
+        "dayjs": "^1.11.13",
+        "echarts": "^5.6.0",
+        "element-plus": "^2.3.14",
         "express": "^4.18.2",
         "jsonwebtoken": "^9.0.2",
         "mqtt": "^5.0.3",
-        "mysql2": "^3.6.0",
+        "mysql2": "^3.12.0",
         "vue": "^3.3.0",
         "vue-router": "^4.2.5",
         "winston": "^3.17.0"
@@ -1878,6 +1882,15 @@
         "node": ">=0.1.90"
       }
     },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@dabh/diagnostics": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
@@ -1899,6 +1912,15 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
+      "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
@@ -1991,6 +2013,31 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.6.9",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+      "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.9"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.6.13",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+      "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.9"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+      "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+      "license": "MIT"
+    },
     "node_modules/@hapi/hoek": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -2732,6 +2779,17 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
     "node_modules/@sideway/address": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -3042,6 +3100,21 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/lodash": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
     "node_modules/@types/mime": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -3182,6 +3255,12 @@
       "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
       "license": "MIT"
     },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.16",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
+      "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
+      "license": "MIT"
+    },
     "node_modules/@types/ws": {
       "version": "8.5.14",
       "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
@@ -3930,6 +4009,94 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@vueuse/core": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
+      "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.16",
+        "@vueuse/metadata": "9.13.0",
+        "@vueuse/shared": "9.13.0",
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
+      "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "9.13.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
+      "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@webassemblyjs/ast": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -4426,6 +4593,12 @@
         "lodash": "^4.17.14"
       }
     },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -6231,6 +6404,12 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "license": "MIT"
     },
+    "node_modules/dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+      "license": "MIT"
+    },
     "node_modules/debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -6687,6 +6866,22 @@
         "safe-buffer": "^5.0.1"
       }
     },
+    "node_modules/echarts": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.1"
+      }
+    },
+    "node_modules/echarts/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -6700,6 +6895,32 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/element-plus": {
+      "version": "2.3.14",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.3.14.tgz",
+      "integrity": "sha512-9yvxUaU4jXf2ZNPdmIxoj/f8BG8CDcGM6oHa9JIqxLjQlfY4bpzR1E5CjNimnOX3rxO93w1TQ0jTVt0RSxh9kA==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.1",
+        "@element-plus/icons-vue": "^2.0.6",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.14.182",
+        "@types/lodash-es": "^4.17.6",
+        "@vueuse/core": "^9.1.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.3",
+        "escape-html": "^1.0.3",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
+        "lodash-unified": "^1.0.2",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
     "node_modules/emittery": {
       "version": "0.13.1",
       "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
@@ -10339,9 +10560,25 @@
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-      "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -10733,6 +10970,12 @@
         "node": ">= 4.0.0"
       }
     },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
     "node_modules/merge-descriptors": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -11387,6 +11630,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/npm-run-path": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@@ -15980,6 +16229,21 @@
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
       "dev": true,
       "license": "ISC"
+    },
+    "node_modules/zrender": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    },
+    "node_modules/zrender/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
     }
   }
 }

+ 6 - 3
package.json

@@ -4,22 +4,25 @@
   "description": "IoT Relay Control Application",
   "main": "app.js",
   "scripts": {
-    "start": "node server/app.js",
-    "dev": "concurrently \"npm run start\" \"npm run serve\"",
+    "dev": "npm run serve",
     "serve": "vue-cli-service serve --port 8080",
     "build": "vue-cli-service build",
     "lint": "eslint --ext .js,.vue src",
     "test": "jest"
   },
   "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
     "axios": "^1.6.2",
     "bcrypt": "^5.1.1",
     "body-parser": "^1.20.2",
     "cors": "^2.8.5",
+    "dayjs": "^1.11.13",
+    "echarts": "^5.6.0",
+    "element-plus": "^2.3.14",
     "express": "^4.18.2",
     "jsonwebtoken": "^9.0.2",
     "mqtt": "^5.0.3",
-    "mysql2": "^3.6.0",
+    "mysql2": "^3.12.0",
     "vue": "^3.3.0",
     "vue-router": "^4.2.5",
     "winston": "^3.17.0"

+ 8 - 1
src/App.vue

@@ -37,6 +37,13 @@
             <span class="text">在线升级</span>
           </router-link>
         </li>
+        <li>
+          <router-link to="/HeaterUsage" @click.stop class="drawer-link" :class="{ active: $route.path === '/HeaterUsage' }">
+            <span class="icon">🚀</span>
+            <span class="text">电暖器使用统计</span>
+          </router-link>
+        </li>
+        <!-- <router-link to="/heater-usage">电暖器使用统计</router-link> -->
       </ul>
     </div>
     <div class="content" :class="{ 'drawer-open': isDrawerOpen }">
@@ -168,4 +175,4 @@ li {
 .drawer.close>ul>li>a>span:first-child{
   margin: 0 auto;
 }
-</style>
+</style>

+ 105 - 0
src/components/HeaterUsage.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="heater-usage-container">
+    <h2>电暖器使用统计</h2>
+    
+    <div class="time-range-selector">
+      <el-radio-group v-model="timeRange">
+        <el-radio-button label="day">日统计</el-radio-button>
+        <el-radio-button label="week">周统计</el-radio-button>
+        <el-radio-button label="month">月统计</el-radio-button>
+      </el-radio-group>
+    </div>
+
+    <div class="filter-container">
+      <el-date-picker
+        v-model="selectedDate"
+        type="date"
+        placeholder="选择日期"
+        @change="fetchUsageData"
+        :disabled-date="disabledDate"
+      />
+    </div>
+
+    <el-table :data="usageData" v-loading="isLoading">
+      <el-table-column prop="room_name" label="房间名称"></el-table-column>
+      <el-table-column prop="duration" label="使用时长">
+        <template #default="{row}">
+          {{ formatDuration(row.duration) }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="date" label="日期"></el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+
+export default {
+  data() {
+    return {
+      timeRange: 'day',
+      usageData: [],
+      refreshInterval: null,
+      isLoading: false
+    };
+  },
+  watch: {
+    timeRange() {
+      this.fetchUsageData();
+    }
+  },
+  mounted() {
+    this.initAutoRefresh();
+  },
+  beforeUnmount() {
+    this.clearAutoRefresh();
+  },
+  methods: {
+    async fetchUsageData() {
+      this.isLoading = true;
+      try {
+        const response = await axios.get('/api/heater-usage', {
+          params: { range: this.timeRange }
+        });
+        // 按时间倒序排列,最新数据在前
+        this.usageData = response.data.sort((a, b) => 
+          new Date(b.date) - new Date(a.date)
+        );
+      } catch (error) {
+        console.error('获取使用数据失败:', error);
+        this.$message.error('获取数据失败,请检查网络连接');
+      } finally {
+        this.isLoading = false;
+      }
+    },
+    // 新增自动刷新逻辑
+    initAutoRefresh() {
+      this.refreshInterval = setInterval(() => {
+        this.fetchUsageData();
+      }, 30000); // 30秒刷新一次
+    },
+    clearAutoRefresh() {
+      if (this.refreshInterval) {
+        clearInterval(this.refreshInterval);
+        this.refreshInterval = null;
+      }
+    },
+    formatDuration(seconds) {
+      const hours = Math.floor(seconds / 3600);
+      const minutes = Math.floor((seconds % 3600) / 60);
+      return `${hours}小时${minutes}分钟`;
+    }
+  }
+};
+</script>
+
+<style>
+.heater-usage-container {
+  padding: 20px;
+}
+
+.time-range-selector {
+  margin-bottom: 20px;
+}
+</style>

+ 386 - 123
src/components/OTAUpgrade.vue

@@ -1,152 +1,415 @@
 <template>
-  <div class="ota-upgrade">
-    <h1>OTA 固件升级</h1>
-    
-    <!-- 设备列表 -->
-    <div class="device-list">
-      <table>
-        <thead>
-          <tr>
-            <th><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
-            <th>设备ID</th>
-            <th>当前固件版本</th>
-            <th>最新版本</th>
-            <th>状态</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="device in devices" :key="device.device_id">
-            <td>
-              <input type="checkbox" v-model="selectedDevices" :value="device.device_id" />
-            </td>
-            <td>{{ device.name }}</td>
-            <td>{{ device.current_firmware_version || '未知' }}</td>
-            <td>{{ device.firmware_version || '未知' }}</td>
-            <td>{{ device.status }}</td>
-          </tr>
-        </tbody>
-      </table>
+  <div class="ota-container">
+    <!-- 头部操作栏 -->
+    <div class="header">
+      <h1>OTA 固件升级管理</h1>
+      <el-button type="primary" @click="showUpload = true">
+        <el-icon><UploadFilled /></el-icon>
+        上传新固件
+      </el-button>
     </div>
 
-    <!-- 操作按钮 -->
-    <div class="actions">
-      <button @click="checkUpdates">检查更新</button>
-      <button @click="startUpgrade" :disabled="selectedDevices.length === 0">开始升级</button>
-    </div>
+    <!-- 设备列表 -->
+    <el-card class="device-list-card">
+      <template #header>
+        <div class="card-header">
+          <span>设备列表</span>
+          <el-button type="text" @click="refreshDeviceList">刷新列表</el-button>
+        </div>
+      </template>
 
-    <!-- 升级日志 -->
-    <div class="upgrade-log">
-      <h3>升级日志</h3>
-      <pre>{{ upgradeLog }}</pre>
-    </div>
+      <el-table :data="deviceList" style="width: 100%">
+        <el-table-column prop="name" label="设备名称" />
+        <el-table-column prop="model" label="设备型号" />
+        <el-table-column prop="systemVersion" label="系统版本" />
+        <el-table-column prop="status" label="状态">
+          <template #default="{ row }">
+            <el-tag :type="row.status === '在线' ? 'success' : 'danger'">
+              {{ row.status }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作">
+          <template #default="{ row }">
+            <el-button size="small" @click="checkDeviceUpdate(row)">
+              检查更新
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+
+
+    <!-- 版本选择卡片 -->
+    <el-card class="version-card">
+      <template #header>
+        <div class="card-header">
+          <span>可用固件版本</span>
+          <el-button type="text" @click="checkUpdates">检查更新</el-button>
+        </div>
+      </template>
+
+      <el-table 
+        :data="filteredVersions"
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column label="版本号" sortable>
+          <template #default="{ row }">
+            {{ row?.version || '未知版本' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="适用设备">
+          <template #default="{ row }">
+            {{ row?.deviceType || '通用设备' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="发布日期" sortable>
+          <template #default="{ row }">
+            {{ row?.releaseDate || '未公布' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作">
+          <template #default="{ row }">
+            <el-button 
+              size="small" 
+              @click="showDetail(row)"
+              :disabled="!isValidRow(row)"
+            >
+              详情
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 推送控制 -->
+    <el-card class="push-control">
+      <template #header>
+        <div class="card-header">推送配置</div>
+      </template>
+      
+      <div class="control-group">
+        <el-select v-model="pushStrategy" placeholder="选择推送策略">
+          <el-option
+            v-for="item in strategies"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        
+        <el-button 
+          type="warning" 
+          :disabled="selectedVersions.length === 0"
+          @click="startUpgrade"
+        >
+          立即推送
+        </el-button>
+      </div>
+    </el-card>
+
+    <!-- 状态监控 -->
+    <el-card class="status-card">
+      <template #header>
+        <div class="card-header">推送状态监控</div>
+      </template>
+      
+      <div class="status-group">
+        <el-progress 
+          type="dashboard" 
+          :percentage="successRate" 
+          :color="colors"
+        />
+        
+        <div class="stats">
+          <div class="stat-item success">
+            <label>成功设备</label>
+            <span>{{ successCount }}</span>
+          </div>
+          <div class="stat-item failure">
+            <label>失败设备</label>
+            <span>{{ failureCount }}</span>
+          </div>
+        </div>
+      </div>
+      
+      <el-divider />
+      
+      <div class="log-container">
+        <h4>实时日志</h4>
+        <div class="log-content">
+          <pre>{{ upgradeLog }}</pre>
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 上传对话框 -->
+    <el-dialog v-model="showUpload" title="固件上传">
+      <el-upload
+        drag
+        :auto-upload="false"
+        :on-change="handleUpload"
+        accept=".bin,.zip"
+      >
+        <el-icon :size="50"><UploadFilled /></el-icon>
+        <div class="el-upload__text">
+          拖拽固件包到此或 <em>点击上传</em>
+        </div>
+      </el-upload>
+      
+      <template #footer>
+        <el-button @click="showUpload = false">取消</el-button>
+        <el-button type="primary" @click="confirmUpload">确认上传</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      devices: [],
-      selectedDevices: [],
-      selectAll: false,
-      upgradeLog: ''
-    };
-  },
-  async mounted() {
-    await this.fetchDevices();
-  },
-  methods: {
-    async fetchDevices() {
-      try {
-        const response = await fetch('/api/devices');
-        if (response.ok) {
-          this.devices = await response.json();
-        }
-      } catch (error) {
-        console.error('获取设备列表失败:', error);
-      }
-    },
-    toggleSelectAll() {
-      if (this.selectAll) {
-        this.selectedDevices = this.devices.map(device => device.device_id);
-      } else {
-        this.selectedDevices = [];
-      }
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { UploadFilled } from '@element-plus/icons-vue'
+
+// 响应式数据
+const showUpload = ref(false)
+const selectedVersions = ref([])
+const pushStrategy = ref('immediate')
+const strategies = [
+  { value: 'immediate', label: '立即推送' },
+  { value: 'schedule', label: '定时推送' },
+  { value: 'gradual', label: '分阶段推送' }
+]
+
+const state = reactive({
+  successRate: 0,
+  successCount: 0,
+  failureCount: 0,
+  upgradeLog: '',
+  versions: [
+    {
+      version: 'v2.1.0',
+      deviceType: 'X-200',
+      releaseDate: '2024-03-20',
+      signed: true
     },
-    async checkUpdates() {
-      try {
-        const response = await fetch('/api/ota/check-update', {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json'
-          },
-          body: JSON.stringify({
-            device_id: this.selectedDeviceId,
-            version: this.currentVersion
-          })
-        });
-        
-        if (response.ok) {
-          const data = await response.json();
-          this.upgradeLog = `检查更新结果:\n${JSON.stringify(data, null, 2)}`;
-        }
-      } catch (error) {
-        console.error('检查更新失败:', error);
-      }
+    {
+      version: 'v2.0.3', 
+      deviceType: 'X-200',
+      releaseDate: '2024-03-15',
+      signed: true
     },
-    async startUpgrade() {
-      try {
-        const response = await fetch('/api/ota/firmware', {
-          method: 'GET',
-          headers: {
-            'Content-Type': 'application/json'
-          }
-        });
-        
-        if (response.ok) {
-          this.upgradeLog = '升级任务已启动...';
-          // 可以添加轮询状态更新的逻辑
-        }
-      } catch (error) {
-        console.error('启动升级失败:', error);
+    {} // 测试空数据
+  ],
+  colors: [
+    { color: '#5cb87a', percentage: 20 },
+    { color: '#e6a23c', percentage: 50 },
+    { color: '#f56c6c', percentage: 80 }
+  ]
+})
+
+// 计算属性过滤无效数据
+const filteredVersions = computed(() => {
+  return (state.versions || [])
+    .filter(item => item && typeof item === 'object')
+    .map(item => ({
+      version: item.version || '未知版本',
+      deviceType: item.deviceType || '通用设备',
+      releaseDate: item.releaseDate || '未公布',
+      ...item
+    }))
+})
+
+// 验证行数据有效性
+const isValidRow = (row) => {
+  return row && typeof row === 'object' && 'version' in row
+}
+
+// 处理选择变化
+const handleSelectionChange = (selection) => {
+  selectedVersions.value = (selection || [])
+    .filter(v => isValidRow(v))
+    .map(v => v?.version || '')
+}
+
+// 显示详情
+const showDetail = (row) => {
+  if (!isValidRow(row)) {
+    ElMessage.warning('无效的版本数据')
+    return
+  }
+  console.log('查看版本详情:', row.version)
+}
+
+// 其他方法
+const checkUpdates = async () => {
+  // 调用API检查更新
+}
+
+const startUpgrade = async () => {
+  // 调用推送API
+}
+
+const handleUpload = (file) => {
+  // 处理文件上传
+}
+
+const confirmUpload = () => {
+  showUpload.value = false
+}
+
+// 当前设备信息
+const currentDevice = reactive({
+  name: '主控设备',
+  model: 'X-200',
+  systemVersion: 'v2.1.0',
+  status: '在线'
+})
+
+// 设备列表
+const deviceList = ref([
+  {
+    name: '设备1',
+    model: 'X-200',
+    systemVersion: 'v2.0.3',
+    status: '在线'
+  },
+  {
+    name: '设备2',
+    model: 'X-200',
+    systemVersion: 'v2.1.0',
+    status: '离线'
+  },
+  {
+    name: '设备3',
+    model: 'X-200',
+    systemVersion: 'v2.0.5',
+    status: '在线'
+  }
+])
+
+// 刷新设备列表
+const refreshDeviceList = async () => {
+  ElMessage.info('正在刷新设备列表...')
+  // 模拟 API 调用
+  setTimeout(() => {
+    deviceList.value = [
+      ...deviceList.value,
+      {
+        name: '设备4',
+        model: 'X-200',
+        systemVersion: 'v2.1.0',
+        status: '在线'
       }
+    ]
+    ElMessage.success('设备列表已刷新')
+  }, 1000)
+}
+
+// 检查设备更新
+const checkDeviceUpdate = (device) => {
+  ElMessage.info(`正在检查 ${device.name} 的更新...`)
+  // 模拟检查更新逻辑
+  setTimeout(() => {
+    if (device.systemVersion !== currentDevice.systemVersion) {
+      ElMessage.warning(`${device.name} 有可用更新`)
+    } else {
+      ElMessage.success(`${device.name} 已是最新版本`)
     }
-  }
-};
+  }, 500)
+}
+
 </script>
 
 <style scoped>
-.ota-upgrade {
+
+.device-info-card {
+  margin-bottom: 20px;
+}
+
+.device-info-content {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+}
+
+.info-item label {
+  font-weight: bold;
+  min-width: 100px;
+}
+
+.device-list-card {
+  margin-bottom: 20px;
+}
+
+.ota-container {
   padding: 20px;
+  max-width: 1200px;
+  margin: 0 auto;
 }
 
-.device-list {
-  margin: 20px 0;
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
 }
 
-table {
-  width: 100%;
-  border-collapse: collapse;
+.version-card, .push-control, .status-card {
+  margin-bottom: 20px;
 }
 
-th, td {
-  padding: 8px;
-  border: 1px solid #ddd;
+.control-group {
+  display: flex;
+  gap: 15px;
 }
 
-.actions {
-  margin: 20px 0;
+.status-group {
+  display: flex;
+  align-items: center;
+  gap: 30px;
 }
 
-button {
-  margin-right: 10px;
-  padding: 8px 16px;
+.stats {
+  flex: 1;
 }
 
-.upgrade-log {
-  margin-top: 20px;
+.stat-item {
   padding: 10px;
-  background-color: #f5f5f5;
-  border: 1px solid #ddd;
+  border-radius: 4px;
+  margin-bottom: 10px;
+  
+  &.success {
+    background-color: #f6ffed;
+    border: 1px solid #b7eb8f;
+  }
+  
+  &.failure {
+    background-color: #fff2f0;
+    border: 1px solid #ffccc7;
+  }
+}
+
+.log-container {
+  max-height: 200px;
+  overflow-y: auto;
+}
+
+.log-content pre {
+  margin: 0;
+  font-family: monospace;
+}
+
+/* 空状态提示 */
+.el-table__empty-block {
+  padding: 20px;
+  color: #666;
 }
-</style>
+</style>

+ 18 - 18
src/components/RoomManagement.vue

@@ -15,8 +15,8 @@
             'sensor-offline': room.occupancy === '人体存在掉线'
           }">
             <h4>{{ room.room_name }}</h4>
-            <p>绑定设备数量: {{ room.deviceCount }}</p>
-            <p>在线设备数量: {{ room.onlineDeviceCount }}</p>
+            <p>设备: {{ room.deviceCount }}</p>
+            <p>在线: {{ room.onlineDeviceCount }}</p>
             <p>
               温度:
               <span v-if="room.hasTemperatureDevice">
@@ -28,28 +28,28 @@
                   </span>
                   <span v-else>温度数据无效</span>
                 </span>
-                <span v-else-if="room.temperatureDeviceStatus === 'offline'">温度离线</span>
+                <span v-else-if="room.temperatureDeviceStatus === 'offline'">离线</span>
               </span>
-              <span v-else>未绑定传感器</span>
+              <span v-else>未绑定</span>
             </p>
             <p>
-              电器状态:
+              电器:
               <span v-if="room.hasTemperatureDevice">
                 <span v-if="room.temperatureDeviceStatus === 'online'">
                   <span v-if="room.switchStatus === 'on'">🔥</span>
                   <span v-else style="filter: grayscale(100%);">🔥</span>
                   {{ room.switchStatus === 'on' ? '开启' : '关闭' }}
                 </span>
-                <span v-else-if="room.temperatureDeviceStatus === 'offline'">继电器离线</span>
+                <span v-else-if="room.temperatureDeviceStatus === 'offline'">离线</span>
               </span>
-              <span v-else>未绑定继电器</span>
+              <span v-else>未绑定</span>
             </p>
             <p>
               房间状态:
               <span v-if="room.occupancy === '有人'" style="color: yellow;">有人</span>
               <span v-else-if="room.occupancy === '无人'" style="color: green;">无人</span>
-              <span v-else-if="room.occupancy === '人体存在掉线'" style="color: red;">人体存在掉线</span>
-              <span v-else style="color: gray;">未绑定人体传感器</span>
+              <span v-else-if="room.occupancy === '人体存在掉线'" style="color: red;">线</span>
+              <span v-else style="color: gray;">未绑定</span>
             </p>
           </div>
         </div>
@@ -63,8 +63,8 @@
             'sensor-offline': room.occupancy === '人体存在掉线'
           }">
             <h4>{{ room.room_name }}</h4>
-            <p>绑定设备数量: {{ room.deviceCount }}</p>
-            <p>在线设备数量: {{ room.onlineDeviceCount }}</p>
+            <p>设备: {{ room.deviceCount }}</p>
+            <p>在线: {{ room.onlineDeviceCount }}</p>
             <p>
               温度:
               <span v-if="room.hasTemperatureDevice">
@@ -76,28 +76,28 @@
                   </span>
                   <span v-else>温度数据无效</span>
                 </span>
-                <span v-else-if="room.temperatureDeviceStatus === 'offline'">温度离线</span>
+                <span v-else-if="room.temperatureDeviceStatus === 'offline'">离线</span>
               </span>
-              <span v-else>未绑定传感器</span>
+              <span v-else>未绑定</span>
             </p>
             <p>
-              电器状态:
+              电器:
               <span v-if="room.hasTemperatureDevice">
                 <span v-if="room.temperatureDeviceStatus === 'online'">
                   <span v-if="room.switchStatus === 'on'">🔥</span>
                   <span v-else style="filter: grayscale(100%);">🔥</span>
                   {{ room.switchStatus === 'on' ? '开启' : '关闭' }}
                 </span>
-                <span v-else-if="room.temperatureDeviceStatus === 'offline'">继电器离线</span>
+                <span v-else-if="room.temperatureDeviceStatus === 'offline'">离线</span>
               </span>
-              <span v-else>未绑定继电器</span>
+              <span v-else>未绑定</span>
             </p>
             <p>
               房间状态:
               <span v-if="room.occupancy === '有人'" style="color: yellow;">有人</span>
               <span v-else-if="room.occupancy === '无人'" style="color: green;">无人</span>
-              <span v-else-if="room.occupancy === '人体存在掉线'" style="color: red;">人体存在掉线</span>
-              <span v-else style="color: gray;">未绑定人体传感器</span>
+              <span v-else-if="room.occupancy === '人体存在掉线'" style="color: red;">线</span>
+              <span v-else style="color: gray;">未绑定</span>
             </p>
           </div>
         </div>

+ 16 - 6
src/main.js

@@ -1,7 +1,17 @@
-import { createApp } from 'vue';
-import App from './App.vue';
-import router from './router';
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
 
-createApp(App)
-  .use(router)
-  .mount('#app');
+// 导入 Element Plus
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+
+// 创建应用实例
+const app = createApp(App)
+
+// 使用路由和 Element Plus
+app.use(router)
+app.use(ElementPlus)
+
+// 挂载应用
+app.mount('#app')

+ 3 - 0
src/router.js

@@ -4,6 +4,7 @@ import SecondPage from './components/SecondPage.vue';
 import RoomManagement from './components/RoomManagement.vue';
 import DeviceManagement from './components/DeviceManagement.vue';
 import OTAUpgrade from './components/OTAUpgrade.vue';
+import HeaterUsage from './components/HeaterUsage.vue';
 import Login from './components/Login.vue';
 
 const routes = [
@@ -12,6 +13,8 @@ const routes = [
   { path: '/', name: 'RoomManagement', component: RoomManagement, meta: { requiresAuth: true } },
   { path: '/DeviceManagement', name: 'DeviceManagement', component: DeviceManagement, meta: { requiresAuth: true } },
   { path: '/OTAUpgrade', name: 'OTAUpgrade', component: OTAUpgrade, meta: { requiresAuth: true } },
+  { path: '/HeaterUsage', name: 'HeaterUsage', component: HeaterUsage, meta: { requiresAuth: true } },
+
   { path: '/login', name: 'Login', component: Login },
 ];
 

+ 21 - 7
src/router/index.js

@@ -4,20 +4,21 @@ import SecondPage from '../components/SecondPage.vue';
 import RoomManagement from '../components/RoomManagement.vue';
 import DeviceManagement from '../components/DeviceManagement.vue';
 import OTAUpgrade from '../components/OTAUpgrade.vue';
+import HeaterUsage from '../components/HeaterUsage.vue';
 import Login from '../components/Login.vue'; // 导入登录组件
 
 const routes = [
   {
-    path: '/home',
-    name: 'Home',
+    path: '/HelloWorld',
+    name: 'HelloWorld',
     component: HelloWorld,
-    meta: { requiresAuth: true }, // 需要登录
+    meta: { requiresAuth: true },
   },
   {
-    path: '/second',
+    path: '/SecondPage',
     name: 'SecondPage',
     component: SecondPage,
-    meta: { requiresAuth: true }, // 需要登录
+    meta: { requiresAuth: true },
   },
   {
     path: '/',
@@ -37,12 +38,25 @@ const routes = [
     component: OTAUpgrade,
     meta: { requiresAuth: true }, // 需要登录
   },
+  {
+    path: '/HeaterUsage',
+    name: 'HeaterUsage',
+    component: HeaterUsage,
+    meta: { requiresAuth: true }, // 需要登录
+  },
+  {
+    path: '/usage-statistics',
+    name: 'UsageStatistics',
+    component: UsageStatistics,
+    meta: { requiresAuth: true }, // 需要登录
+  },
   {
     path: '/login',
     name: 'Login',
     component: Login,
   },
-];
+
+]
 
 const router = createRouter({
   history: createWebHistory(),
@@ -59,4 +73,4 @@ router.beforeEach((to, from, next) => {
   }
 });
 
-export default router;
+export default router;