SettingsTab.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. <template>
  2. <div class="settings-tab">
  3. <!-- Unlock form -->
  4. <div v-if="!unlocked" class="unlock-form">
  5. <h3>Enter Password to Access Settings</h3>
  6. <div class="form-row">
  7. <input
  8. v-model="password"
  9. type="password"
  10. placeholder="Device password"
  11. class="input"
  12. @keyup.enter="unlock"
  13. />
  14. <button @click="unlock" class="btn primary">Unlock</button>
  15. </div>
  16. </div>
  17. <!-- Settings form -->
  18. <div v-else class="settings-form">
  19. <div class="section">
  20. <h3>Mode</h3>
  21. <div class="radio-group">
  22. <label>
  23. <input type="radio" v-model="settings.mode" value="cloud" />
  24. Cloud Mode - config from server
  25. </label>
  26. <label>
  27. <input type="radio" v-model="settings.mode" value="lan" />
  28. LAN Mode - local config only
  29. </label>
  30. </div>
  31. </div>
  32. <div class="section">
  33. <h3>Network - eth0</h3>
  34. <div class="form-row">
  35. <label>Mode</label>
  36. <select v-model="settings.eth0_mode" class="input">
  37. <option value="dhcp">DHCP</option>
  38. <option value="static">Static</option>
  39. </select>
  40. </div>
  41. <template v-if="settings.eth0_mode === 'static'">
  42. <div class="form-row">
  43. <label>IP Address</label>
  44. <input v-model="settings.eth0_ip" type="text" class="input" placeholder="192.168.1.100/24" />
  45. </div>
  46. <div class="form-row">
  47. <label>Gateway</label>
  48. <input v-model="settings.eth0_gateway" type="text" class="input" placeholder="192.168.1.1" />
  49. </div>
  50. <div class="form-row">
  51. <label>DNS</label>
  52. <input v-model="settings.eth0_dns" type="text" class="input" placeholder="8.8.8.8" />
  53. </div>
  54. </template>
  55. </div>
  56. <div class="section">
  57. <h3>WiFi Client</h3>
  58. <div class="form-row">
  59. <label>SSID</label>
  60. <input v-model="settings.wifi_ssid" type="text" class="input" placeholder="Network name" />
  61. </div>
  62. <div class="form-row">
  63. <label>Password</label>
  64. <input v-model="settings.wifi_psk" type="password" class="input" placeholder="WiFi password" />
  65. </div>
  66. </div>
  67. <div class="section">
  68. <h3>NTP</h3>
  69. <div class="form-row">
  70. <label>Servers</label>
  71. <input v-model="settings.ntp_servers" type="text" class="input" placeholder="pool.ntp.org, time.google.com" />
  72. </div>
  73. </div>
  74. <div class="section" v-if="settings.mode === 'lan'">
  75. <h3>Endpoints (LAN Mode)</h3>
  76. <div class="form-row">
  77. <label>BLE Endpoint</label>
  78. <input v-model="settings.endpoint_ble" type="text" class="input" placeholder="http://192.168.1.10:5000/ble" />
  79. </div>
  80. <div class="form-row">
  81. <label>WiFi Endpoint</label>
  82. <input v-model="settings.endpoint_wifi" type="text" class="input" placeholder="http://192.168.1.10:5000/wifi" />
  83. </div>
  84. </div>
  85. <div class="actions">
  86. <button @click="save" class="btn primary">Apply Settings</button>
  87. <button @click="reset" class="btn">Reset</button>
  88. </div>
  89. <div v-if="message?.text" class="message" :class="message.type">
  90. <span v-if="message.type === 'info'" class="spinner"></span>
  91. <span v-else-if="message.type === 'success'" class="icon">&#10004;</span>
  92. <span v-else-if="message.type === 'error'" class="icon">&#10006;</span>
  93. {{ message.text }}
  94. </div>
  95. </div>
  96. </div>
  97. </template>
  98. <script setup>
  99. import { ref, reactive, watch } from 'vue'
  100. const props = defineProps({
  101. config: Object,
  102. unlocked: Boolean,
  103. message: Object
  104. })
  105. const emit = defineEmits(['unlock', 'save'])
  106. const password = ref('')
  107. const settingsLoaded = ref(false)
  108. const settings = reactive({
  109. mode: 'cloud',
  110. eth0_mode: 'dhcp',
  111. eth0_ip: '',
  112. eth0_gateway: '',
  113. eth0_dns: '',
  114. wifi_ssid: '',
  115. wifi_psk: '',
  116. ntp_servers: 'pool.ntp.org',
  117. endpoint_ble: '',
  118. endpoint_wifi: ''
  119. })
  120. // Load settings only ONCE when unlocked (not on every config poll)
  121. watch(() => props.unlocked, (unlocked) => {
  122. if (unlocked && !settingsLoaded.value && props.config) {
  123. loadFromConfig(props.config)
  124. settingsLoaded.value = true
  125. }
  126. }, { immediate: true })
  127. function loadFromConfig(cfg) {
  128. if (!cfg) return
  129. settings.mode = cfg.mode || 'cloud'
  130. if (cfg.network) {
  131. settings.eth0_mode = cfg.network.eth0?.mode || 'dhcp'
  132. settings.eth0_ip = cfg.network.eth0?.static?.address || ''
  133. settings.eth0_gateway = cfg.network.eth0?.static?.gateway || ''
  134. settings.eth0_dns = cfg.network.eth0?.static?.dns?.join(', ') || ''
  135. settings.wifi_ssid = cfg.network.wifi?.ssid || ''
  136. settings.ntp_servers = cfg.network.ntp?.servers?.join(', ') || 'pool.ntp.org'
  137. }
  138. }
  139. function unlock() {
  140. emit('unlock', password.value)
  141. }
  142. function save() {
  143. emit('save', { ...settings })
  144. }
  145. function reset() {
  146. // Reset to config values
  147. loadFromConfig(props.config)
  148. }
  149. </script>
  150. <style scoped>
  151. .settings-tab {
  152. max-width: 600px;
  153. }
  154. .unlock-form {
  155. background: #16213e;
  156. border-radius: 8px;
  157. padding: 2rem;
  158. text-align: center;
  159. }
  160. .unlock-form h3 {
  161. margin-bottom: 1rem;
  162. color: #888;
  163. }
  164. .settings-form {
  165. display: flex;
  166. flex-direction: column;
  167. gap: 1.5rem;
  168. }
  169. .section {
  170. background: #16213e;
  171. border-radius: 8px;
  172. padding: 1rem;
  173. border: 1px solid #0f3460;
  174. }
  175. .section h3 {
  176. font-size: 0.875rem;
  177. color: #888;
  178. margin-bottom: 1rem;
  179. text-transform: uppercase;
  180. letter-spacing: 0.05em;
  181. }
  182. .form-row {
  183. display: flex;
  184. align-items: center;
  185. gap: 1rem;
  186. margin-bottom: 0.75rem;
  187. }
  188. .form-row:last-child {
  189. margin-bottom: 0;
  190. }
  191. .form-row label {
  192. min-width: 100px;
  193. font-size: 0.875rem;
  194. color: #aaa;
  195. }
  196. .input {
  197. flex: 1;
  198. padding: 0.5rem;
  199. background: #0f3460;
  200. border: 1px solid #1a4a7a;
  201. border-radius: 4px;
  202. color: #eee;
  203. font-size: 0.875rem;
  204. }
  205. .input:focus {
  206. outline: none;
  207. border-color: #e94560;
  208. }
  209. select.input {
  210. cursor: pointer;
  211. }
  212. .radio-group {
  213. display: flex;
  214. flex-direction: column;
  215. gap: 0.5rem;
  216. }
  217. .radio-group label {
  218. display: flex;
  219. align-items: center;
  220. gap: 0.5rem;
  221. font-size: 0.875rem;
  222. color: #aaa;
  223. cursor: pointer;
  224. }
  225. .radio-group input[type="radio"] {
  226. accent-color: #e94560;
  227. }
  228. .actions {
  229. display: flex;
  230. gap: 1rem;
  231. margin-top: 1rem;
  232. }
  233. .btn {
  234. padding: 0.75rem 1.5rem;
  235. background: #0f3460;
  236. border: 1px solid #1a4a7a;
  237. border-radius: 4px;
  238. color: #eee;
  239. cursor: pointer;
  240. font-size: 0.875rem;
  241. transition: all 0.2s;
  242. }
  243. .btn:hover {
  244. background: #1a4a7a;
  245. }
  246. .btn.primary {
  247. background: #e94560;
  248. border-color: #e94560;
  249. }
  250. .btn.primary:hover {
  251. background: #d63850;
  252. }
  253. .message {
  254. margin-top: 1rem;
  255. padding: 0.75rem 1rem;
  256. border-radius: 6px;
  257. font-size: 0.875rem;
  258. display: flex;
  259. align-items: center;
  260. gap: 0.5rem;
  261. }
  262. .spinner {
  263. width: 16px;
  264. height: 16px;
  265. border: 2px solid #3b82f6;
  266. border-top-color: transparent;
  267. border-radius: 50%;
  268. animation: spin 0.8s linear infinite;
  269. }
  270. @keyframes spin {
  271. to { transform: rotate(360deg); }
  272. }
  273. .icon {
  274. font-size: 1rem;
  275. }
  276. .message.info {
  277. background: #1e3a5f;
  278. border: 1px solid #3b82f6;
  279. color: #93c5fd;
  280. }
  281. .message.success {
  282. background: #14532d;
  283. border: 1px solid #22c55e;
  284. color: #86efac;
  285. }
  286. .message.error {
  287. background: #450a0a;
  288. border: 1px solid #ef4444;
  289. color: #fca5a5;
  290. }
  291. </style>