moki 4 дней назад
Родитель
Сommit
10de453d45
3 измененных файлов с 644 добавлено и 133 удалено
  1. 420 0
      package-lock.json
  2. 1 0
      package.json
  3. 223 133
      src/views/DeviceConfig.vue

+ 420 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "@vitejs/plugin-vue": "^6.0.6",
         "@vitejs/plugin-vue": "^6.0.6",
         "@vue/tsconfig": "^0.9.1",
         "@vue/tsconfig": "^0.9.1",
         "playwright": "^1.61.1",
         "playwright": "^1.61.1",
+        "sass": "^1.101.0",
         "typescript": "~6.0.2",
         "typescript": "~6.0.2",
         "vite": "^8.0.12",
         "vite": "^8.0.12",
         "vue-tsc": "^3.2.8"
         "vue-tsc": "^3.2.8"
@@ -197,6 +198,334 @@
         "url": "https://github.com/sponsors/Boshen"
         "url": "https://github.com/sponsors/Boshen"
       }
       }
     },
     },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+      "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3",
+        "is-glob": "^4.0.3",
+        "node-addon-api": "^7.0.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.6",
+        "@parcel/watcher-darwin-arm64": "2.5.6",
+        "@parcel/watcher-darwin-x64": "2.5.6",
+        "@parcel/watcher-freebsd-x64": "2.5.6",
+        "@parcel/watcher-linux-arm-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm-musl": "2.5.6",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm64-musl": "2.5.6",
+        "@parcel/watcher-linux-x64-glibc": "2.5.6",
+        "@parcel/watcher-linux-x64-musl": "2.5.6",
+        "@parcel/watcher-win32-arm64": "2.5.6",
+        "@parcel/watcher-win32-ia32": "2.5.6",
+        "@parcel/watcher-win32-x64": "2.5.6"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+      "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+      "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+      "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+      "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+      "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+      "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+      "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+      "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+      "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+      "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+      "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+      "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+      "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
     "node_modules/@rolldown/binding-android-arm64": {
     "node_modules/@rolldown/binding-android-arm64": {
       "version": "1.0.3",
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
       "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
@@ -781,6 +1110,22 @@
         "node": ">= 0.4"
         "node": ">= 0.4"
       }
       }
     },
     },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/combined-stream": {
     "node_modules/combined-stream": {
       "version": "1.0.8",
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1126,6 +1471,38 @@
         "node": ">= 6"
         "node": ">= 6"
       }
       }
     },
     },
+    "node_modules/immutable": {
+      "version": "5.1.8",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.8.tgz",
+      "integrity": "sha512-TM5YqrGeTsVIPPpILzeqZ8D2Zc2TvNgSDi88zPF2a4cyqQdWV/wVWBDRDbNzzrLeRWScrFcOX9lW2iX6GOtUDw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/is-plain-object": {
     "node_modules/is-plain-object": {
       "version": "3.0.1",
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
@@ -1502,6 +1879,14 @@
       "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
       "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
     "node_modules/path-browserify": {
     "node_modules/path-browserify": {
       "version": "1.0.1",
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -1612,6 +1997,20 @@
         "node": ">=10"
         "node": ">=10"
       }
       }
     },
     },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
     "node_modules/resize-observer-polyfill": {
     "node_modules/resize-observer-polyfill": {
       "version": "1.5.1",
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
       "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -1652,6 +2051,27 @@
         "@rolldown/binding-win32-x64-msvc": "1.0.3"
         "@rolldown/binding-win32-x64-msvc": "1.0.3"
       }
       }
     },
     },
+    "node_modules/sass": {
+      "version": "1.101.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz",
+      "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^5.0.0",
+        "immutable": "^5.1.5",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
     "node_modules/scroll-into-view-if-needed": {
     "node_modules/scroll-into-view-if-needed": {
       "version": "2.2.31",
       "version": "2.2.31",
       "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
       "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "@vitejs/plugin-vue": "^6.0.6",
     "@vitejs/plugin-vue": "^6.0.6",
     "@vue/tsconfig": "^0.9.1",
     "@vue/tsconfig": "^0.9.1",
     "playwright": "^1.61.1",
     "playwright": "^1.61.1",
+    "sass": "^1.101.0",
     "typescript": "~6.0.2",
     "typescript": "~6.0.2",
     "vite": "^8.0.12",
     "vite": "^8.0.12",
     "vue-tsc": "^3.2.8"
     "vue-tsc": "^3.2.8"

+ 223 - 133
src/views/DeviceConfig.vue

@@ -59,6 +59,12 @@ const LIGHT_MODES = [
   {key: 'off', label: 'Off'},
   {key: 'off', label: 'Off'},
 ]
 ]
 
 
+const GPIO_OPTIONS = [
+  {value: 2, label: 'GPIO2'},
+  {value: 3, label: 'GPIO3'},
+  {value: 4, label: 'GPIO4'},
+]
+
 const bleWifiForm = reactive({ssid: '', password: ''})
 const bleWifiForm = reactive({ssid: '', password: ''})
 const bleMqttForm = reactive({
 const bleMqttForm = reactive({
   broker: '',
   broker: '',
@@ -71,6 +77,7 @@ const bleMqttForm = reactive({
   topicConfig: 'agent/status/config',
   topicConfig: 'agent/status/config',
 })
 })
 const blePinForm = reactive({red: 4, green: 3, yellow: 2})
 const blePinForm = reactive({red: 4, green: 3, yellow: 2})
+let blePendingAction: 'save' | 'restart' | null = null
 
 
 const mqttWifiForm = reactive({ssid: '', password: ''})
 const mqttWifiForm = reactive({ssid: '', password: ''})
 const mqttMqttForm = reactive({
 const mqttMqttForm = reactive({
@@ -101,6 +108,17 @@ watch(bleConfig, (cfg) => {
   }
   }
 })
 })
 
 
+watch(bleState, (state) => {
+  if (state === 'disconnected' && blePendingAction) {
+    if (blePendingAction === 'save') {
+      message.success('保存成功,请待设备启动成功后手动连接设备!')
+    } else if (blePendingAction === 'restart') {
+      message.success('重启成功,请待设备启动成功后手动连接设备!')
+    }
+    blePendingAction = null
+  }
+})
+
 watch(mqttConfig, (cfg) => {
 watch(mqttConfig, (cfg) => {
   if (cfg) {
   if (cfg) {
     mqttWifiForm.ssid = cfg.wifi_ssid || ''
     mqttWifiForm.ssid = cfg.wifi_ssid || ''
@@ -134,18 +152,20 @@ async function handleBleSave() {
     pin_yellow: blePinForm.yellow,
     pin_yellow: blePinForm.yellow,
   }
   }
   try {
   try {
+    blePendingAction = 'save'
     await bleSaveConfig(cfg)
     await bleSaveConfig(cfg)
-    message.success('配置已保存,设备将重启')
   } catch {
   } catch {
+    blePendingAction = null
     message.error('保存失败')
     message.error('保存失败')
   }
   }
 }
 }
 
 
 async function handleBleRestart() {
 async function handleBleRestart() {
   try {
   try {
+    blePendingAction = 'restart'
     await bleRestartDevice()
     await bleRestartDevice()
-    message.success('重启指令已发送')
   } catch {
   } catch {
+    blePendingAction = null
     message.error('重启失败')
     message.error('重启失败')
   }
   }
 }
 }
@@ -168,7 +188,7 @@ async function handleMqttSave() {
   }
   }
   try {
   try {
     await mqttSaveConfig(cfg)
     await mqttSaveConfig(cfg)
-    message.success('配置已保存并推送')
+    message.success('保存成功,请待设备启动成功后手动连接设备!')
   } catch {
   } catch {
     message.error('保存失败')
     message.error('保存失败')
   }
   }
@@ -177,7 +197,7 @@ async function handleMqttSave() {
 async function handleMqttRestart() {
 async function handleMqttRestart() {
   try {
   try {
     await mqttRestartDevice()
     await mqttRestartDevice()
-    message.success('重启指令已发送')
+    message.success('重启成功,请待设备启动成功后手动连接设备!')
   } catch {
   } catch {
     message.error('重启失败')
     message.error('重启失败')
   }
   }
@@ -255,96 +275,114 @@ onMounted(() => window.addEventListener('resize', onResize))
                   <a-tag color="processing">{{ bleMode || '-' }}</a-tag>
                   <a-tag color="processing">{{ bleMode || '-' }}</a-tag>
                 </a-descriptions-item>
                 </a-descriptions-item>
                 <a-descriptions-item label="通信">
                 <a-descriptions-item label="通信">
-                  {{ bleConfig?.comm_mode === 1 ? 'MQTT' : 'BLE-only' }}
+                  <a-tag v-if="bleConfig?.mqtt_broker" color="success">MQTT</a-tag>
+                  <a-tag v-else color="warning">BLE-only</a-tag>
                 </a-descriptions-item>
                 </a-descriptions-item>
               </a-descriptions>
               </a-descriptions>
             </a-card>
             </a-card>
 
 
-            <a-card class="section-card" size="small" title="灯效模式">
-              <div :class="isMobile ? 'mode-grid-mobile' : 'mode-grid-desktop'">
-                <a-button
-                    v-for="m in LIGHT_MODES"
-                    :key="m.key"
-                    :type="bleMode === m.key ? 'primary' : 'default'"
-                    class="mode-btn"
-                    @click="bleSetMode(m.key)"
-                >
-                  {{ m.label }}
-                </a-button>
-              </div>
-            </a-card>
-
-            <a-card class="section-card" size="small" title="WiFi 配置">
-              <a-form layout="vertical">
-                <a-form-item label="SSID">
-                  <a-input v-model:value="bleWifiForm.ssid" placeholder="WiFi 名称"/>
-                </a-form-item>
-                <a-form-item label="密码">
-                  <a-input-password v-model:value="bleWifiForm.password" placeholder="WiFi 密码"/>
-                </a-form-item>
-              </a-form>
-            </a-card>
+            <a-row :gutter="8" class="equal-height-row">
+              <a-col :span="isMobile ? 24 : 14" class="equal-height-col">
+                <a-card class="section-card equal-height-card" size="small" title="灯效模式">
+                  <div class="mode-grid">
+                    <a-button
+                        v-for="m in LIGHT_MODES"
+                        :key="m.key"
+                        :type="bleMode === m.key ? 'primary' : 'default'"
+                        class="mode-btn"
+                        @click="bleSetMode(m.key)"
+                    >
+                      {{ m.label }}
+                    </a-button>
+                  </div>
+                </a-card>
+              </a-col>
+              <a-col :span="isMobile ? 24 : 10" class="equal-height-col">
+                <a-card class="section-card equal-height-card" size="small" title="WiFi 配置">
+                  <a-form layout="vertical">
+                    <a-form-item label="SSID">
+                      <a-input v-model:value="bleWifiForm.ssid" placeholder="WiFi 名称"/>
+                    </a-form-item>
+                    <a-form-item label="密码">
+                      <a-input-password v-model:value="bleWifiForm.password" placeholder="WiFi 密码"/>
+                    </a-form-item>
+                  </a-form>
+                </a-card>
+              </a-col>
+            </a-row>
 
 
             <a-card class="section-card" size="small" title="MQTT 配置">
             <a-card class="section-card" size="small" title="MQTT 配置">
               <a-form layout="vertical">
               <a-form layout="vertical">
-                <a-form-item label="Broker">
-                  <a-input v-model:value="bleMqttForm.broker" placeholder="192.168.1.100"/>
-                </a-form-item>
                 <a-row :gutter="12">
                 <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="Broker">
+                      <a-input v-model:value="bleMqttForm.broker" placeholder="192.168.1.100"/>
+                    </a-form-item>
+                  </a-col>
                   <a-col :span="12">
                   <a-col :span="12">
                     <a-form-item label="端口">
                     <a-form-item label="端口">
                       <a-input-number v-model:value="bleMqttForm.port" :max="65535" :min="1" style="width: 100%"/>
                       <a-input-number v-model:value="bleMqttForm.port" :max="65535" :min="1" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
+                </a-row>
+                <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="订阅主题">
+                      <a-input v-model:value="bleMqttForm.topic" placeholder="agent/status"/>
+                    </a-form-item>
+                  </a-col>
                   <a-col :span="12">
                   <a-col :span="12">
                     <a-form-item label="Client ID">
                     <a-form-item label="Client ID">
                       <a-input v-model:value="bleMqttForm.client" placeholder="AI-Light"/>
                       <a-input v-model:value="bleMqttForm.client" placeholder="AI-Light"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                 </a-row>
                 </a-row>
-                <a-form-item label="用户名">
-                  <a-input v-model:value="bleMqttForm.username" placeholder="可选"/>
-                </a-form-item>
-                <a-form-item label="密码">
-                  <a-input-password v-model:value="bleMqttForm.password" placeholder="可选"/>
-                </a-form-item>
-                <a-form-item label="订阅主题">
-                  <a-input v-model:value="bleMqttForm.topic" placeholder="agent/status"/>
-                </a-form-item>
-                <a-form-item label="状态发布主题">
-                  <a-input v-model:value="bleMqttForm.statusTopic" placeholder="openCodeLight/status"/>
-                </a-form-item>
-                <a-form-item label="配置订阅主题">
-                  <a-input v-model:value="bleMqttForm.topicConfig" placeholder="agent/status/config"/>
-                </a-form-item>
+                <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="用户名">
+                      <a-input v-model:value="bleMqttForm.username" placeholder="可选"/>
+                    </a-form-item>
+                  </a-col>
+                  <a-col :span="12">
+                    <a-form-item label="密码">
+                      <a-input-password v-model:value="bleMqttForm.password" placeholder="可选"/>
+                    </a-form-item>
+                  </a-col>
+                </a-row>
+                <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="状态发布主题">
+                      <a-input v-model:value="bleMqttForm.statusTopic" placeholder="openCodeLight/status"/>
+                    </a-form-item>
+                  </a-col>
+                  <a-col :span="12">
+                    <a-form-item label="配置订阅主题">
+                      <a-input v-model:value="bleMqttForm.topicConfig" placeholder="agent/status/config"/>
+                    </a-form-item>
+                  </a-col>
+                </a-row>
               </a-form>
               </a-form>
             </a-card>
             </a-card>
 
 
-            <a-card class="section-card" size="small" title="引脚配置(灯序)">
+            <a-card class="section-card" size="small" title="灯序配置">
               <a-form layout="vertical">
               <a-form layout="vertical">
                 <a-row :gutter="12">
                 <a-row :gutter="12">
                   <a-col :span="8">
                   <a-col :span="8">
                     <a-form-item label="红灯引脚">
                     <a-form-item label="红灯引脚">
-                      <a-input-number v-model:value="blePinForm.red" :max="21" :min="0" style="width: 100%"/>
+                      <a-select v-model:value="blePinForm.red" :options="GPIO_OPTIONS" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                   <a-col :span="8">
                   <a-col :span="8">
                     <a-form-item label="绿灯引脚">
                     <a-form-item label="绿灯引脚">
-                      <a-input-number v-model:value="blePinForm.green" :max="21" :min="0" style="width: 100%"/>
+                      <a-select v-model:value="blePinForm.green" :options="GPIO_OPTIONS" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                   <a-col :span="8">
                   <a-col :span="8">
                     <a-form-item label="黄灯引脚">
                     <a-form-item label="黄灯引脚">
-                      <a-input-number v-model:value="blePinForm.yellow" :max="21" :min="0" style="width: 100%"/>
+                      <a-select v-model:value="blePinForm.yellow" :options="GPIO_OPTIONS" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                 </a-row>
                 </a-row>
-                <a-alert
-                    banner
-                    message="默认接线:红=IO4、绿=IO3、黄=IO2。修改引脚后需确认硬件接线对应。"
-                    show-icon
-                    type="info"
-                />
               </a-form>
               </a-form>
             </a-card>
             </a-card>
 
 
@@ -383,7 +421,8 @@ onMounted(() => window.addEventListener('resize', onResize))
                   <a-tag color="processing">{{ bleMode || '-' }}</a-tag>
                   <a-tag color="processing">{{ bleMode || '-' }}</a-tag>
                 </a-descriptions-item>
                 </a-descriptions-item>
                 <a-descriptions-item label="通信">
                 <a-descriptions-item label="通信">
-                  {{ bleConfig?.comm_mode === 1 ? 'MQTT' : 'BLE-only' }}
+                  <a-tag v-if="bleConfig?.mqtt_broker" color="success">MQTT</a-tag>
+                  <a-tag v-else color="warning">BLE-only</a-tag>
                 </a-descriptions-item>
                 </a-descriptions-item>
               </a-descriptions>
               </a-descriptions>
             </a-card>
             </a-card>
@@ -462,95 +501,114 @@ onMounted(() => window.addEventListener('resize', onResize))
                 <a-descriptions-item label="模式">
                 <a-descriptions-item label="模式">
                   <a-tag color="processing">{{ mqttMode || '-' }}</a-tag>
                   <a-tag color="processing">{{ mqttMode || '-' }}</a-tag>
                 </a-descriptions-item>
                 </a-descriptions-item>
-                <a-descriptions-item label="通信">MQTT</a-descriptions-item>
+                <a-descriptions-item label="通信">
+                  <a-tag color="success">MQTT</a-tag>
+                </a-descriptions-item>
               </a-descriptions>
               </a-descriptions>
             </a-card>
             </a-card>
 
 
-            <a-card class="section-card" size="small" title="灯效模式">
-              <div :class="isMobile ? 'mode-grid-mobile' : 'mode-grid-desktop'">
-                <a-button
-                    v-for="m in LIGHT_MODES"
-                    :key="m.key"
-                    :type="mqttMode === m.key ? 'primary' : 'default'"
-                    class="mode-btn"
-                    @click="mqttSetMode(m.key)"
-                >
-                  {{ m.label }}
-                </a-button>
-              </div>
-            </a-card>
-
-            <a-card class="section-card" size="small" title="WiFi 配置">
-              <a-form layout="vertical">
-                <a-form-item label="SSID">
-                  <a-input v-model:value="mqttWifiForm.ssid" placeholder="WiFi 名称"/>
-                </a-form-item>
-                <a-form-item label="密码">
-                  <a-input-password v-model:value="mqttWifiForm.password" placeholder="WiFi 密码"/>
-                </a-form-item>
-              </a-form>
-            </a-card>
+            <a-row :gutter="8" class="equal-height-row">
+              <a-col :span="isMobile ? 24 : 14" class="equal-height-col">
+                <a-card class="section-card equal-height-card" size="small" title="灯效模式">
+                  <div class="mode-grid">
+                    <a-button
+                        v-for="m in LIGHT_MODES"
+                        :key="m.key"
+                        :type="mqttMode === m.key ? 'primary' : 'default'"
+                        class="mode-btn"
+                        @click="mqttSetMode(m.key)"
+                    >
+                      {{ m.label }}
+                    </a-button>
+                  </div>
+                </a-card>
+              </a-col>
+              <a-col :span="isMobile ? 24 : 10" class="equal-height-col">
+                <a-card class="section-card equal-height-card" size="small" title="WiFi 配置">
+                  <a-form layout="vertical">
+                    <a-form-item label="SSID">
+                      <a-input v-model:value="mqttWifiForm.ssid" placeholder="WiFi 名称"/>
+                    </a-form-item>
+                    <a-form-item label="密码">
+                      <a-input-password v-model:value="mqttWifiForm.password" placeholder="WiFi 密码"/>
+                    </a-form-item>
+                  </a-form>
+                </a-card>
+              </a-col>
+            </a-row>
 
 
             <a-card class="section-card" size="small" title="MQTT 配置">
             <a-card class="section-card" size="small" title="MQTT 配置">
               <a-form layout="vertical">
               <a-form layout="vertical">
-                <a-form-item label="Broker">
-                  <a-input v-model:value="mqttMqttForm.broker" placeholder="192.168.1.100"/>
-                </a-form-item>
                 <a-row :gutter="12">
                 <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="Broker">
+                      <a-input v-model:value="mqttMqttForm.broker" placeholder="192.168.1.100"/>
+                    </a-form-item>
+                  </a-col>
                   <a-col :span="12">
                   <a-col :span="12">
                     <a-form-item label="端口">
                     <a-form-item label="端口">
                       <a-input-number v-model:value="mqttMqttForm.port" :max="65535" :min="1" style="width: 100%"/>
                       <a-input-number v-model:value="mqttMqttForm.port" :max="65535" :min="1" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
+                </a-row>
+                <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="订阅主题">
+                      <a-input v-model:value="mqttMqttForm.topic" placeholder="agent/status"/>
+                    </a-form-item>
+                  </a-col>
                   <a-col :span="12">
                   <a-col :span="12">
                     <a-form-item label="Client ID">
                     <a-form-item label="Client ID">
                       <a-input v-model:value="mqttMqttForm.client" placeholder="AI-Light"/>
                       <a-input v-model:value="mqttMqttForm.client" placeholder="AI-Light"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                 </a-row>
                 </a-row>
-                <a-form-item label="用户名">
-                  <a-input v-model:value="mqttMqttForm.username" placeholder="可选"/>
-                </a-form-item>
-                <a-form-item label="密码">
-                  <a-input-password v-model:value="mqttMqttForm.password" placeholder="可选"/>
-                </a-form-item>
-                <a-form-item label="订阅主题">
-                  <a-input v-model:value="mqttMqttForm.topic" placeholder="agent/status"/>
-                </a-form-item>
-                <a-form-item label="状态发布主题">
-                  <a-input v-model:value="mqttMqttForm.statusTopic" placeholder="openCodeLight/status"/>
-                </a-form-item>
-                <a-form-item label="配置订阅主题">
-                  <a-input v-model:value="mqttMqttForm.topicConfig" placeholder="agent/status/config"/>
-                </a-form-item>
+                <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="用户名">
+                      <a-input v-model:value="mqttMqttForm.username" placeholder="可选"/>
+                    </a-form-item>
+                  </a-col>
+                  <a-col :span="12">
+                    <a-form-item label="密码">
+                      <a-input-password v-model:value="mqttMqttForm.password" placeholder="可选"/>
+                    </a-form-item>
+                  </a-col>
+                </a-row>
+                <a-row :gutter="12">
+                  <a-col :span="12">
+                    <a-form-item label="状态发布主题">
+                      <a-input v-model:value="mqttMqttForm.statusTopic" placeholder="openCodeLight/status"/>
+                    </a-form-item>
+                  </a-col>
+                  <a-col :span="12">
+                    <a-form-item label="配置订阅主题">
+                      <a-input v-model:value="mqttMqttForm.topicConfig" placeholder="agent/status/config"/>
+                    </a-form-item>
+                  </a-col>
+                </a-row>
               </a-form>
               </a-form>
             </a-card>
             </a-card>
 
 
-            <a-card class="section-card" size="small" title="引脚配置(灯序)">
+            <a-card class="section-card" size="small" title="灯序配置">
               <a-form layout="vertical">
               <a-form layout="vertical">
                 <a-row :gutter="12">
                 <a-row :gutter="12">
                   <a-col :span="8">
                   <a-col :span="8">
                     <a-form-item label="红灯引脚">
                     <a-form-item label="红灯引脚">
-                      <a-input-number v-model:value="mqttPinForm.red" :max="21" :min="0" style="width: 100%"/>
+                      <a-select v-model:value="mqttPinForm.red" :options="GPIO_OPTIONS" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                   <a-col :span="8">
                   <a-col :span="8">
                     <a-form-item label="绿灯引脚">
                     <a-form-item label="绿灯引脚">
-                      <a-input-number v-model:value="mqttPinForm.green" :max="21" :min="0" style="width: 100%"/>
+                      <a-select v-model:value="mqttPinForm.green" :options="GPIO_OPTIONS" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                   <a-col :span="8">
                   <a-col :span="8">
                     <a-form-item label="黄灯引脚">
                     <a-form-item label="黄灯引脚">
-                      <a-input-number v-model:value="mqttPinForm.yellow" :max="21" :min="0" style="width: 100%"/>
+                      <a-select v-model:value="mqttPinForm.yellow" :options="GPIO_OPTIONS" style="width: 100%"/>
                     </a-form-item>
                     </a-form-item>
                   </a-col>
                   </a-col>
                 </a-row>
                 </a-row>
-                <a-alert
-                    banner
-                    message="默认接线:红=IO4、绿=IO3、黄=IO2。修改引脚后需确认硬件接线对应。"
-                    show-icon
-                    type="info"
-                />
               </a-form>
               </a-form>
             </a-card>
             </a-card>
 
 
@@ -591,7 +649,9 @@ onMounted(() => window.addEventListener('resize', onResize))
                 <a-descriptions-item label="模式">
                 <a-descriptions-item label="模式">
                   <a-tag color="processing">{{ mqttMode || '-' }}</a-tag>
                   <a-tag color="processing">{{ mqttMode || '-' }}</a-tag>
                 </a-descriptions-item>
                 </a-descriptions-item>
-                <a-descriptions-item label="通信">MQTT</a-descriptions-item>
+                <a-descriptions-item label="通信">
+                  <a-tag color="success">MQTT</a-tag>
+                </a-descriptions-item>
               </a-descriptions>
               </a-descriptions>
             </a-card>
             </a-card>
 
 
@@ -614,7 +674,7 @@ onMounted(() => window.addEventListener('resize', onResize))
   </div>
   </div>
 </template>
 </template>
 
 
-<style scoped>
+<style lang="scss" scoped>
 .device-config-page {
 .device-config-page {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
@@ -630,6 +690,19 @@ onMounted(() => window.addEventListener('resize', onResize))
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+  :deep(.ant-card-head) {
+    min-height: 46px;
+  }
+
+  :deep(.ant-card-body) {
+    height: calc(100% - 46px);
+    overflow: hidden;
+  }
+
+  :deep(.ant-form-item) {
+    margin-bottom: 10px;
+  }
 }
 }
 
 
 .section-card {
 .section-card {
@@ -637,13 +710,20 @@ onMounted(() => window.addEventListener('resize', onResize))
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
 }
 }
 
 
-.mode-grid-mobile {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 8px;
+.equal-height-row {
+  display: flex;
+  align-items: stretch;
+}
+
+.equal-height-col {
+  display: flex;
+}
+
+.equal-height-card {
+  flex: 1;
 }
 }
 
 
-.mode-grid-desktop {
+.mode-grid {
   display: grid;
   display: grid;
   grid-template-columns: repeat(3, 1fr);
   grid-template-columns: repeat(3, 1fr);
   gap: 8px;
   gap: 8px;
@@ -660,9 +740,10 @@ onMounted(() => window.addEventListener('resize', onResize))
   padding: 12px;
   padding: 12px;
   font-family: 'SF Mono', 'Fira Code', monospace;
   font-family: 'SF Mono', 'Fira Code', monospace;
   font-size: 11px;
   font-size: 11px;
+  line-height: 1.8;
   flex: 1;
   flex: 1;
+  min-height: 0;
   overflow-y: auto;
   overflow-y: auto;
-  line-height: 1.8;
 }
 }
 
 
 .log-line {
 .log-line {
@@ -680,11 +761,15 @@ onMounted(() => window.addEventListener('resize', onResize))
   display: flex;
   display: flex;
   gap: 8px;
   gap: 8px;
   align-items: stretch;
   align-items: stretch;
+  height: calc(100% - 52px);
+  overflow: hidden;
 }
 }
 
 
 .config-col {
 .config-col {
   flex: 1;
   flex: 1;
   min-width: 0;
   min-width: 0;
+  overflow-y: auto;
+  overflow-x: hidden;
 }
 }
 
 
 .side-col {
 .side-col {
@@ -692,28 +777,33 @@ onMounted(() => window.addEventListener('resize', onResize))
   flex-shrink: 0;
   flex-shrink: 0;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-}
-
-.log-card {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-}
-
-.log-card :deep(.ant-card-body) {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
+  overflow: hidden;
+
+  .log-card {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 0;
+    height: 0;
+
+    :deep(.ant-card-body) {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      height: 0;
+      overflow: hidden;
+    }
+  }
 }
 }
 
 
 @media (max-width: 767px) {
 @media (max-width: 767px) {
-  .mode-grid-mobile {
+  .mode-grid {
     grid-template-columns: repeat(2, 1fr);
     grid-template-columns: repeat(2, 1fr);
   }
   }
 }
 }
 
 
 @media (min-width: 1200px) {
 @media (min-width: 1200px) {
-  .mode-grid-desktop {
+  .mode-grid {
     grid-template-columns: repeat(3, 1fr);
     grid-template-columns: repeat(3, 1fr);
   }
   }
 }
 }