index.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset='UTF-8'>
  5. <meta name='viewport'
  6. content='width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover'>
  7. <meta name="apple-mobile-web-app-capable" content="yes">
  8. <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  9. <title>Audio Control Panel</title>
  10. <style>
  11. @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Inter:wght@300;400;600&display=swap');
  12. :root {
  13. --bg-color: #111111;
  14. --panel-bg: #1a1a1a;
  15. --gold: #d4af37;
  16. --gold-dark: #b5952f;
  17. --text-main: #f0f0f0;
  18. --text-dim: #888888;
  19. --border: #333333;
  20. }
  21. * {
  22. box-sizing: border-box;
  23. margin: 0;
  24. padding: 0
  25. }
  26. body,
  27. button,
  28. input {
  29. font-family: 'Songti SC', 'STSong', 'SimSun', 'Times New Roman', serif;
  30. }
  31. html,
  32. body {
  33. background: var(--bg-color);
  34. color: var(--text-main);
  35. min-height: 100vh;
  36. overflow-x: hidden;
  37. -ms-overflow-style: none;
  38. /* IE and Edge */
  39. scrollbar-width: none;
  40. /* Firefox */
  41. }
  42. *::-webkit-scrollbar {
  43. display: none !important;
  44. width: 0 !important;
  45. height: 0 !important;
  46. }
  47. /* Leather texture overlay */
  48. body::before {
  49. content: "";
  50. position: fixed;
  51. top: 0;
  52. left: 0;
  53. width: 100%;
  54. height: 100%;
  55. background-image: url('data:image/svg+xml;utf8,%3Csvg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"%3E%3Cfilter id="noiseFilter"%3E%3CfeTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/%3E%3C/filter%3E%3Crect width="100%25" height="100%25" filter="url(%23noiseFilter)" opacity="0.05"/%3E%3C/svg%3E');
  56. pointer-events: none;
  57. z-index: -1;
  58. }
  59. .wrap {
  60. max-width: 500px;
  61. margin: 0 auto;
  62. padding: calc(10px + env(safe-area-inset-top)) 20px calc(20px + env(safe-area-inset-bottom)) 20px;
  63. }
  64. /* Dashboard Styling */
  65. .brand {
  66. font-family: 'Cinzel', serif;
  67. font-size: 32px;
  68. font-weight: 700;
  69. color: var(--gold);
  70. text-align: center;
  71. margin-bottom: 10px;
  72. letter-spacing: 2px;
  73. text-shadow: 0 2px 10px rgba(212, 175, 55, 0.2);
  74. }
  75. .subtitle {
  76. text-align: center;
  77. font-size: 12px;
  78. color: var(--text-dim);
  79. text-transform: uppercase;
  80. letter-spacing: 4px;
  81. margin-bottom: 40px;
  82. }
  83. .dashboard-card {
  84. background: var(--panel-bg);
  85. border: 1px solid var(--border);
  86. border-radius: 12px;
  87. padding: 30px 20px;
  88. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  89. margin-bottom: 20px;
  90. }
  91. /* Status row */
  92. .status-row {
  93. display: flex;
  94. justify-content: space-between;
  95. align-items: flex-start;
  96. margin-bottom: 30px;
  97. padding-bottom: 20px;
  98. border-bottom: 1px solid var(--border);
  99. }
  100. .status-item {
  101. text-align: center;
  102. flex: 1;
  103. }
  104. .status-label {
  105. font-size: 13px;
  106. color: var(--text-dim);
  107. text-transform: uppercase;
  108. letter-spacing: 1px;
  109. margin-bottom: 5px;
  110. }
  111. .status-val {
  112. font-size: 16px;
  113. font-weight: 600;
  114. color: var(--gold);
  115. margin-top: 4px;
  116. height: 22px;
  117. line-height: 22px;
  118. display: flex;
  119. justify-content: center;
  120. align-items: center;
  121. }
  122. /* Now Playing Box */
  123. .now-playing {
  124. background: #000;
  125. border-radius: 12px;
  126. padding: 25px 15px;
  127. margin-bottom: 30px;
  128. border: 1px solid var(--border);
  129. text-align: center;
  130. box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.9);
  131. min-height: 120px;
  132. display: flex;
  133. flex-direction: column;
  134. justify-content: center;
  135. }
  136. .np-title {
  137. font-size: 24px;
  138. font-weight: bold;
  139. color: var(--gold);
  140. margin-bottom: 8px;
  141. }
  142. .np-artist {
  143. font-size: 18px;
  144. color: var(--text-main);
  145. margin-bottom: 4px;
  146. }
  147. .np-album {
  148. font-size: 15px;
  149. color: var(--text-dim);
  150. }
  151. /* Source Toggle */
  152. .source-toggle {
  153. display: flex;
  154. background: #000;
  155. border-radius: 8px;
  156. padding: 5px;
  157. margin-bottom: 30px;
  158. border: 1px solid var(--border);
  159. }
  160. .source-toggle button {
  161. flex: 1;
  162. padding: 12px;
  163. background: transparent;
  164. border: none;
  165. color: var(--text-dim);
  166. font-weight: 600;
  167. font-size: 16px;
  168. cursor: pointer;
  169. border-radius: 6px;
  170. transition: all 0.3s ease;
  171. text-transform: uppercase;
  172. letter-spacing: 1px;
  173. }
  174. .source-toggle button.active {
  175. background: var(--gold);
  176. color: #000;
  177. box-shadow: 0 0 15px rgba(212, 175, 55, 0.3);
  178. }
  179. /* Volume Slider */
  180. .vol-container {
  181. margin-bottom: 40px;
  182. }
  183. .vol-header {
  184. display: flex;
  185. justify-content: space-between;
  186. margin-bottom: 15px;
  187. }
  188. .vol-header label {
  189. font-size: 15px;
  190. color: var(--text-dim);
  191. text-transform: uppercase;
  192. letter-spacing: 2px;
  193. }
  194. .vol-header span {
  195. color: var(--gold);
  196. font-weight: 600;
  197. font-size: 16px;
  198. }
  199. input[type=range] {
  200. -webkit-appearance: none;
  201. appearance: none;
  202. width: 100%;
  203. height: 6px;
  204. background: linear-gradient(to right, var(--gold) var(--val, 50%), #000 var(--val, 50%));
  205. border-radius: 3px;
  206. outline: none;
  207. border: 1px solid var(--border);
  208. }
  209. input[type=range]::-webkit-slider-thumb {
  210. -webkit-appearance: none;
  211. appearance: none;
  212. width: 24px;
  213. height: 24px;
  214. border-radius: 50%;
  215. background: radial-gradient(circle at 30% 30%, #ffd700, var(--gold-dark));
  216. cursor: pointer;
  217. box-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
  218. border: 2px solid #fff;
  219. }
  220. /* Buttons */
  221. .btn-gold {
  222. display: block;
  223. width: 100%;
  224. padding: 16px;
  225. background: transparent;
  226. border: 1px solid var(--gold);
  227. color: var(--gold);
  228. text-transform: uppercase;
  229. letter-spacing: 2px;
  230. font-weight: 600;
  231. font-size: 16px;
  232. border-radius: 8px;
  233. cursor: pointer;
  234. transition: all 0.3s ease;
  235. text-align: center;
  236. text-decoration: none;
  237. }
  238. .btn-gold:hover,
  239. .btn-gold:active {
  240. background: var(--gold);
  241. color: #000;
  242. }
  243. .settings-btn {
  244. background: transparent;
  245. border: none;
  246. color: var(--text-dim);
  247. text-decoration: underline;
  248. cursor: pointer;
  249. font-size: 14px;
  250. display: block;
  251. margin: 30px auto 0;
  252. letter-spacing: 1px;
  253. }
  254. /* Settings Modal (Overlay) */
  255. .settings-overlay {
  256. position: fixed;
  257. top: 0;
  258. left: 0;
  259. width: 100%;
  260. height: 100%;
  261. background: rgba(0, 0, 0, 0.9);
  262. backdrop-filter: blur(5px);
  263. z-index: 100;
  264. display: none;
  265. overflow-y: auto;
  266. -webkit-overflow-scrolling: touch;
  267. overscroll-behavior-y: contain;
  268. padding: calc(20px + env(safe-area-inset-top)) 20px calc(20px + env(safe-area-inset-bottom)) 20px;
  269. }
  270. .settings-overlay.active {
  271. display: block;
  272. }
  273. .close-btn {
  274. color: #000;
  275. background: var(--gold);
  276. width: 38px;
  277. height: 38px;
  278. border-radius: 50%;
  279. font-size: 24px;
  280. font-weight: bold;
  281. border: none;
  282. float: right;
  283. cursor: pointer;
  284. line-height: 38px;
  285. text-align: center;
  286. box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
  287. margin-top: -5px;
  288. margin-right: -5px;
  289. }
  290. .settings-content {
  291. max-width: 500px;
  292. margin: 0 auto;
  293. padding-top: 15px;
  294. }
  295. /* Old UI Styles adapted for Settings */
  296. .card {
  297. background: #1a1a1a;
  298. border-radius: 12px;
  299. padding: 20px;
  300. margin-bottom: 16px;
  301. border: 1px solid #333;
  302. }
  303. .card h2 {
  304. font-size: 16px;
  305. font-weight: 600;
  306. margin-bottom: 16px;
  307. color: var(--gold);
  308. }
  309. .form-group {
  310. margin-bottom: 14px
  311. }
  312. .form-group label {
  313. display: block;
  314. font-size: 12px;
  315. color: #888;
  316. margin-bottom: 6px;
  317. }
  318. input[type=text],
  319. input[type=password] {
  320. width: 100%;
  321. padding: 12px;
  322. background: #000;
  323. border: 1px solid #333;
  324. border-radius: 6px;
  325. color: #fff;
  326. font-size: 14px
  327. }
  328. input:focus {
  329. outline: none;
  330. border-color: var(--gold)
  331. }
  332. .btn {
  333. display: inline-block;
  334. padding: 12px 20px;
  335. border: none;
  336. border-radius: 6px;
  337. font-size: 13px;
  338. cursor: pointer;
  339. background: #333;
  340. color: #fff;
  341. width: 100%;
  342. margin-bottom: 10px;
  343. }
  344. .btn-primary {
  345. background: var(--gold);
  346. color: #000;
  347. font-weight: 600;
  348. }
  349. .wifi-list {
  350. max-height: 180px;
  351. overflow-y: auto;
  352. -webkit-overflow-scrolling: touch;
  353. margin: 12px 0;
  354. }
  355. .wifi-item {
  356. padding: 12px;
  357. background: #000;
  358. margin-bottom: 4px;
  359. border-radius: 6px;
  360. cursor: pointer;
  361. display: flex;
  362. justify-content: space-between;
  363. }
  364. .wifi-item.sel {
  365. border: 1px solid var(--gold);
  366. }
  367. .msg {
  368. padding: 10px;
  369. border-radius: 6px;
  370. margin-top: 10px;
  371. font-size: 12px
  372. }
  373. .msg.ok {
  374. color: #4ade80;
  375. }
  376. .msg.err {
  377. color: #f87171;
  378. }
  379. .info-grid {
  380. display: grid;
  381. grid-template-columns: 1fr 1fr;
  382. gap: 10px
  383. }
  384. .info-item {
  385. background: #000;
  386. padding: 10px;
  387. border-radius: 6px;
  388. }
  389. .info-item label {
  390. font-size: 10px;
  391. color: #888;
  392. display: block;
  393. }
  394. .info-item span {
  395. font-size: 13px;
  396. font-weight: 500
  397. }
  398. </style>
  399. <script>
  400. (function () {
  401. const canvas = document.createElement('canvas');
  402. canvas.width = 180; canvas.height = 180;
  403. const ctx = canvas.getContext('2d');
  404. // Background
  405. ctx.fillStyle = '#111'; ctx.fillRect(0, 0, 180, 180);
  406. // Gold circle
  407. ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 8;
  408. ctx.beginPath(); ctx.arc(90, 90, 65, 0, 2 * Math.PI); ctx.stroke();
  409. // Musical note
  410. ctx.fillStyle = '#d4af37';
  411. ctx.font = 'bold 85px Arial';
  412. ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  413. ctx.fillText('♪', 90, 98);
  414. ctx.lineWidth = 3;
  415. ctx.strokeText('♪', 90, 98);
  416. const link = document.createElement('link');
  417. link.rel = 'apple-touch-icon';
  418. link.href = canvas.toDataURL('image/png');
  419. document.head.appendChild(link);
  420. })();
  421. </script>
  422. </head>
  423. <body>
  424. <!-- MAIN DASHBOARD -->
  425. <div class="wrap">
  426. <div class="brand" id="brand-name">音响系统</div>
  427. <div id="disconnected-banner"
  428. style="display:none; position:fixed; bottom:30px; left:50%; transform:translateX(-50%); width:90%; max-width:400px; z-index:9999; background:rgba(255, 68, 68, 0.5); color:white; border:1px solid rgba(255, 68, 68, 0.3); padding:12px; text-align:center; border-radius:12px; font-weight:600; font-size:14px; backdrop-filter:blur(12px); box-shadow:0 8px 20px rgba(0,0,0,0.3);">
  429. ⚠️ 设备未连接,正在重试...
  430. </div>
  431. <div class="dashboard-card">
  432. <div class="status-row">
  433. <div class="status-item">
  434. <div class="status-label">电池电量</div>
  435. <div class="status-val" id="dash-bat">--%</div>
  436. </div>
  437. <div class="status-item" style="border-left:1px solid #333; border-right:1px solid #333;">
  438. <div class="status-label">网络状态</div>
  439. <div class="status-val" id="dash-net">离线</div>
  440. </div>
  441. <div class="status-item">
  442. <div class="status-label">运行状态</div>
  443. <div class="status-val" id="dash-status" style="color:#4ade80;">正常</div>
  444. </div>
  445. <div class="status-item" style="border-left:1px solid #333;">
  446. <div class="status-label">播放源</div>
  447. <div class="status-val" id="dash-conn" style="color:#aaa;">未连接</div>
  448. </div>
  449. </div>
  450. <!-- Now Playing Box -->
  451. <div class="now-playing" id="now-playing-box">
  452. <div id="np-header" style="font-size:14px; color:var(--text-dim); margin-bottom:10px; letter-spacing:2px;">正在播放:
  453. </div>
  454. <div class="np-title" id="np-title">等待播放...</div>
  455. <div class="np-artist" id="np-artist">-</div>
  456. <div class="np-album" id="np-album"></div>
  457. <div id="np-progress-container" style="display:none; margin-top:25px; padding:0 15px;">
  458. <div
  459. style="display:flex; justify-content:space-between; font-size:13px; color:var(--text-dim); margin-bottom:8px;">
  460. <span id="np-pos" style="font-variant-numeric: tabular-nums;">00:00</span>
  461. <span id="np-dur" style="font-variant-numeric: tabular-nums;">00:00</span>
  462. </div>
  463. <div
  464. style="width:100%; height:6px; background:#1a1a1a; border-radius:3px; overflow:hidden; border: 1px solid #333;">
  465. <div id="np-progress-bar"
  466. style="width:0%; height:100%; background:var(--gold); transition:width 1s linear; box-shadow: 0 0 8px rgba(212, 175, 55, 0.5);">
  467. </div>
  468. </div>
  469. </div>
  470. </div>
  471. <div class="source-toggle">
  472. <button id="src-btn-0" onclick="setSource(0)">AirPlay / 蓝牙</button>
  473. <button id="src-btn-1" onclick="setSource(1)">AUX</button>
  474. </div>
  475. <div class="vol-container">
  476. <div class="vol-header">
  477. <label>主音量</label>
  478. <span id="vol-val">--%</span>
  479. </div>
  480. <input type="range" id="main-vol" min="0" max="100" value="50" style="--val: 50%"
  481. oninput="document.getElementById('vol-val').textContent=this.value+'%'; this.style.setProperty('--val', this.value+'%')"
  482. onchange="setVolume(this.value)">
  483. </div>
  484. <div class="vol-container">
  485. <div class="vol-header">
  486. <label>低音音量</label>
  487. <span id="bass-val">--%</span>
  488. </div>
  489. <input type="range" id="bass-vol" min="0" max="100" value="50" style="--val: 50%"
  490. oninput="document.getElementById('bass-val').textContent=this.value+'%'; this.style.setProperty('--val', this.value+'%')"
  491. onchange="setBassVolume(this.value)">
  492. </div>
  493. <button class="btn-gold" onclick="clearBT()" style="margin-bottom:15px;">重新配对蓝牙</button>
  494. </div>
  495. <button class="settings-btn" onclick="openSettings(event)">高级设置 (ADVANCED SETTINGS)</button>
  496. </div>
  497. <!-- SETTINGS MODAL -->
  498. <div id="settings-overlay" class="settings-overlay">
  499. <div class="settings-content">
  500. <button class="close-btn"
  501. onclick="document.getElementById('settings-overlay').classList.remove('active')">×</button>
  502. <div style="clear:both; margin-bottom:5px;"></div>
  503. <div class='card' style="padding-bottom: 30px;">
  504. <h2>音频高级设置</h2>
  505. <div class="vol-container" style="margin-bottom:0;">
  506. <div class="vol-header">
  507. <label style="color:#888;">提示音音量 (Prompt Volume)</label>
  508. <span id="prompt-val" style="color:#ddd;">--%</span>
  509. </div>
  510. <input type="range" id="prompt-vol" min="0" max="100" value="50" style="--val: 50%"
  511. oninput="document.getElementById('prompt-val').textContent=this.value+'%'; this.style.setProperty('--val', this.value+'%')"
  512. onchange="setPromptVolume(this.value)">
  513. </div>
  514. </div>
  515. <div class='card'>
  516. <h2>WiFi</h2>
  517. <button class='btn' onclick='scanWiFi()'>扫描</button>
  518. <div id='wifi-list' class='wifi-list'></div>
  519. <div class='form-group'><label>网络名称 (SSID)</label><input type='text' id='wifi-ssid' autocomplete='off'
  520. autocorrect='off' autocapitalize='off' spellcheck='false'></div>
  521. <div class='form-group'><label>WiFi 密码</label><input type='password' id='wifi-pass' autocomplete='new-password'>
  522. </div>
  523. <button class='btn btn-primary' onclick='saveWiFi()'>连接</button>
  524. <div id='wifi-msg'></div>
  525. </div>
  526. <div class='card'>
  527. <h2>设备名称</h2>
  528. <div class='form-group'><label>修改将显示在AirPlay和蓝牙设备列表以及网络名中</label><input type='text' id='dev-name'
  529. autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false'></div>
  530. <button class='btn btn-primary' onclick='saveName()'>保存名称</button>
  531. <div id='name-msg'></div>
  532. </div>
  533. <div class='card'>
  534. <h2>固件升级</h2>
  535. <input type='file' id='fw-file' accept='.bin' style='display:none'>
  536. <button class='btn' onclick='document.getElementById("fw-file").click()'>选择文件: <span
  537. id='fw-name'>未选择</span></button>
  538. <button class='btn btn-primary' id='ota-btn' onclick='startOTA()' disabled>上传并升级固件</button>
  539. <div id='ota-msg'></div>
  540. </div>
  541. <div class='card'>
  542. <h2>系统信息</h2>
  543. <div class='info-grid'>
  544. <div class='info-item'><label>IP 地址</label><span id='info-ip'>-</span></div>
  545. <div class='info-item'><label>设备名称</label><span id='info-name'>-</span></div>
  546. <div class='info-item'><label>固件版本</label><span id='info-fw'>-</span></div>
  547. <div class='info-item'><label>剩余内存</label><span id='info-heap'>-</span></div>
  548. </div>
  549. <button class='btn' onclick='restart()' style='margin-top:15px; background:#dc3545;'>重启设备</button>
  550. </div>
  551. </div>
  552. </div>
  553. <script>
  554. let initialLoadComplete = false;
  555. let isDragging = false;
  556. // 监听全局触摸/鼠标按下事件,判断是否正在拖动滑块
  557. document.addEventListener('mousedown', (e) => { if (e.target && e.target.type === 'range') isDragging = true; });
  558. document.addEventListener('touchstart', (e) => { if (e.target && e.target.type === 'range') isDragging = true; }, { passive: true });
  559. document.addEventListener('mouseup', () => { setTimeout(() => { isDragging = false; }, 100); });
  560. document.addEventListener('touchend', () => { setTimeout(() => { isDragging = false; }, 100); });
  561. // --- Dashboard Logic ---
  562. function openSettings(e) {
  563. if (e) e.preventDefault();
  564. // Delay slightly to prevent mobile ghost clicks from falling through and focusing inputs
  565. setTimeout(() => {
  566. document.getElementById('settings-overlay').classList.add('active');
  567. }, 150);
  568. }
  569. async function fetchStatus() {
  570. try {
  571. let r = await fetch('/api/status');
  572. if (!r.ok) throw new Error("Status failed");
  573. let d = await r.json();
  574. document.getElementById('disconnected-banner').style.display = 'none';
  575. document.getElementById('dash-bat').textContent = d.battery + '%' + (d.charging ? ' ⚡' : '');
  576. let statusEl = document.getElementById('dash-status');
  577. if (d.sys_status) {
  578. statusEl.textContent = d.sys_status;
  579. if (d.sys_status === '正常') {
  580. statusEl.style.color = '#4ade80';
  581. } else if (d.sys_status === '低电量') {
  582. statusEl.style.color = '#f87171';
  583. } else if (d.sys_status === '充电中') {
  584. statusEl.style.color = '#facc15';
  585. } else {
  586. statusEl.style.color = '#fb923c'; // fallback for warning/OTA
  587. }
  588. }
  589. let connEl = document.getElementById('dash-conn');
  590. if (d.conn_type) {
  591. connEl.textContent = d.conn_type;
  592. if (d.conn_type === '未连接') {
  593. connEl.style.color = '#aaa';
  594. } else {
  595. connEl.style.color = '#4ade80';
  596. }
  597. }
  598. let volEl = document.getElementById('main-vol');
  599. if (!isDragging && document.activeElement !== volEl) {
  600. volEl.value = d.volume;
  601. volEl.style.setProperty('--val', d.volume + '%');
  602. document.getElementById('vol-val').textContent = d.volume + '%';
  603. }
  604. let bassEl = document.getElementById('bass-vol');
  605. if (!isDragging && document.activeElement !== bassEl && d.bass_volume !== undefined) {
  606. bassEl.value = d.bass_volume;
  607. bassEl.style.setProperty('--val', d.bass_volume + '%');
  608. document.getElementById('bass-val').textContent = d.bass_volume + '%';
  609. }
  610. let promptEl = document.getElementById('prompt-vol');
  611. if (!isDragging && document.activeElement !== promptEl && d.prompt_volume !== undefined) {
  612. promptEl.value = d.prompt_volume;
  613. promptEl.style.setProperty('--val', d.prompt_volume + '%');
  614. document.getElementById('prompt-val').textContent = d.prompt_volume + '%';
  615. }
  616. let npHeader = document.getElementById('np-header');
  617. let npTitle = document.getElementById('np-title');
  618. let npArtist = document.getElementById('np-artist');
  619. let npAlbum = document.getElementById('np-album');
  620. if (d.source === 1) {
  621. npHeader.textContent = '当前模式:AUX 输入';
  622. npTitle.textContent = 'AUX 模拟输入';
  623. npArtist.textContent = '不支持歌曲信息显示';
  624. npArtist.style.color = 'var(--text-dim)';
  625. npAlbum.textContent = '';
  626. } else {
  627. npHeader.textContent = '正在播放:';
  628. npArtist.style.color = 'var(--text-main)';
  629. if (d.title && d.title !== "") {
  630. npTitle.textContent = d.title;
  631. npArtist.textContent = d.artist || '未知歌手';
  632. npAlbum.textContent = d.album || '';
  633. if (d.duration && d.duration > 0) {
  634. function formatSecs(secs) {
  635. if (secs == null || isNaN(secs)) return "00:00";
  636. let m = Math.floor(secs / 60);
  637. let s = Math.floor(secs % 60);
  638. return (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s);
  639. }
  640. document.getElementById('np-progress-container').style.display = 'block';
  641. document.getElementById('np-dur').textContent = formatSecs(d.duration);
  642. document.getElementById('np-pos').textContent = formatSecs(d.position);
  643. let pct = (d.position / d.duration) * 100;
  644. if (pct > 100) pct = 100;
  645. document.getElementById('np-progress-bar').style.width = pct + '%';
  646. } else {
  647. document.getElementById('np-progress-container').style.display = 'none';
  648. }
  649. } else {
  650. npTitle.textContent = '等待播放...';
  651. npArtist.textContent = '-';
  652. npAlbum.textContent = '';
  653. document.getElementById('np-progress-container').style.display = 'none';
  654. }
  655. }
  656. document.getElementById('src-btn-0').className = d.source === 0 ? 'active' : '';
  657. document.getElementById('src-btn-1').className = d.source === 1 ? 'active' : '';
  658. initialLoadComplete = true;
  659. } catch (e) {
  660. document.getElementById('disconnected-banner').style.display = 'block';
  661. }
  662. }
  663. async function setVolume(val) {
  664. if (!initialLoadComplete) return;
  665. try {
  666. await fetch('/api/control', {
  667. method: 'POST', headers: { 'Content-Type': 'application/json' },
  668. body: JSON.stringify({ action: 'set_volume', volume: parseInt(val) })
  669. });
  670. } catch (e) { }
  671. }
  672. async function setBassVolume(val) {
  673. if (!initialLoadComplete) return;
  674. try {
  675. await fetch('/api/control', {
  676. method: 'POST', headers: { 'Content-Type': 'application/json' },
  677. body: JSON.stringify({ action: 'set_bass_volume', volume: parseInt(val) })
  678. });
  679. } catch (e) { }
  680. }
  681. async function setPromptVolume(val) {
  682. if (!initialLoadComplete) return;
  683. try {
  684. await fetch('/api/control', {
  685. method: 'POST', headers: { 'Content-Type': 'application/json' },
  686. body: JSON.stringify({ action: 'set_prompt_volume', volume: parseInt(val) })
  687. });
  688. } catch (e) { }
  689. }
  690. async function setSource(src) {
  691. if (!initialLoadComplete) return;
  692. document.getElementById('src-btn-0').className = src === 0 ? 'active' : '';
  693. document.getElementById('src-btn-1').className = src === 1 ? 'active' : '';
  694. try {
  695. await fetch('/api/control', {
  696. method: 'POST', headers: { 'Content-Type': 'application/json' },
  697. body: JSON.stringify({ action: 'set_source', source: src })
  698. });
  699. } catch (e) { }
  700. }
  701. async function clearBT() {
  702. if (!confirm('将重置蓝牙连接,请确认')) return;
  703. try {
  704. await fetch('/api/control', {
  705. method: 'POST', headers: { 'Content-Type': 'application/json' },
  706. body: JSON.stringify({ action: 'clear_bt' })
  707. });
  708. alert('设备蓝牙已可被发现。');
  709. } catch (e) { }
  710. }
  711. // --- Old Settings Logic ---
  712. function msg(id, txt, type) { var e = document.getElementById(id); if (e) { e.innerHTML = '<div class="msg ' + type + '">' + txt + '</div>'; setTimeout(function () { e.innerHTML = ''; }, 4000); } }
  713. async function scanWiFi() {
  714. var l = document.getElementById('wifi-list'); l.innerHTML = '<div class="msg">扫描中...</div>';
  715. try {
  716. var r = await fetch('/api/wifi/scan'); var d = await r.json();
  717. if (d.success && d.networks.length > 0) {
  718. l.innerHTML = ''; d.networks.forEach(function (n) {
  719. var i = document.createElement('div'); i.className = 'wifi-item';
  720. i.innerHTML = '<span>' + n.ssid + '</span><span style="color:#888">' + n.rssi + ' dBm</span>';
  721. i.onclick = function () { document.querySelectorAll('.wifi-item').forEach(function (x) { x.classList.remove('sel'); }); i.classList.add('sel'); document.getElementById('wifi-ssid').value = n.ssid; };
  722. l.appendChild(i);
  723. });
  724. }
  725. else { l.innerHTML = '<div class="msg">未找到网络</div>'; }
  726. }
  727. catch (e) { l.innerHTML = '<div class="msg err">扫描失败</div>'; }
  728. }
  729. async function saveWiFi() {
  730. var s = document.getElementById('wifi-ssid').value.trim(); var p = document.getElementById('wifi-pass').value;
  731. if (!s) { msg('wifi-msg', '请输入网络名称', 'err'); return; }
  732. try {
  733. var r = await fetch('/api/wifi/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ssid: s, password: p }) });
  734. var d = await r.json(); if (d.success) { msg('wifi-msg', '已保存!正在重启...', 'ok'); } else { msg('wifi-msg', d.error, 'err'); }
  735. }
  736. catch (e) { msg('wifi-msg', '出错', 'err'); }
  737. }
  738. async function saveName() {
  739. var n = document.getElementById('dev-name').value.trim(); if (!n) return;
  740. try {
  741. var r = await fetch('/api/device/name', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: n }) });
  742. var d = await r.json(); if (d.success) { msg('name-msg', '已保存!重启生效。', 'ok'); } else { msg('name-msg', d.error, 'err'); }
  743. }
  744. catch (e) { }
  745. }
  746. async function startOTA() {
  747. var f = document.getElementById('fw-file').files[0]; if (!f) return;
  748. document.getElementById('ota-btn').disabled = true; msg('ota-msg', '正在上传...', 'ok');
  749. try { await fetch('/api/ota/update', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: f }); msg('ota-msg', '完成!正在重启...', 'ok'); }
  750. catch (e) { }
  751. }
  752. async function restart() {
  753. if (!confirm('确认重启设备?')) return;
  754. try { await fetch('/api/system/restart', { method: 'POST' }); } catch (e) { }
  755. }
  756. async function loadInfo() {
  757. try {
  758. var r = await fetch('/api/system/info'); var d = await r.json();
  759. if (d.success) {
  760. var i = d.info;
  761. document.getElementById('dash-net').textContent = i.wifi_connected ? i.ip : (i.eth_connected ? i.ip : '离线');
  762. document.getElementById('info-ip').textContent = i.ip || '-';
  763. document.getElementById('info-name').textContent = i.device_name || '-';
  764. document.getElementById('info-heap').textContent = Math.round(i.free_heap / 1024) + ' KB';
  765. document.getElementById('info-fw').textContent = i.firmware_version || '-';
  766. document.getElementById('dev-name').placeholder = i.device_name || '';
  767. if (i.device_name) document.getElementById('brand-name').textContent = i.device_name.toUpperCase();
  768. }
  769. } catch (e) { }
  770. }
  771. window.onload = function () {
  772. document.getElementById('fw-file').onchange = function (e) {
  773. var f = e.target.files[0];
  774. document.getElementById('fw-name').textContent = f ? f.name : '未选择文件';
  775. document.getElementById('ota-btn').disabled = !f;
  776. };
  777. loadInfo();
  778. fetchStatus();
  779. setInterval(fetchStatus, 2000);
  780. setInterval(loadInfo, 10000);
  781. };
  782. </script>
  783. </body>
  784. </html>