index.html 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>展厅控制</title>
  7. <style>
  8. :root {
  9. --primary-color: #4ecdc4;
  10. --secondary-color: #ff6b6b;
  11. --accent-color: #feca57;
  12. --dark-text: #333;
  13. --light-text: #fff;
  14. --bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  15. --card-bg: #ffffff;
  16. }
  17. * {
  18. margin: 0;
  19. padding: 0;
  20. box-sizing: border-box;
  21. }
  22. body {
  23. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  24. background: var(--bg-gradient);
  25. min-height: 100vh;
  26. padding: 20px;
  27. color: var(--dark-text);
  28. }
  29. .container {
  30. max-width: 1000px;
  31. margin: 0 auto;
  32. }
  33. .header {
  34. text-align: center;
  35. margin-bottom: 30px;
  36. color: #2c3e50;
  37. padding: 20px;
  38. background: rgba(255, 255, 255, 0.8);
  39. border-radius: 15px;
  40. box-shadow: 0 4px 6px rgba(0,0,0,0.05);
  41. }
  42. .header h1 {
  43. font-size: 2.2em;
  44. margin-bottom: 10px;
  45. color: #1a252f;
  46. }
  47. .main-section {
  48. background: var(--card-bg);
  49. border-radius: 15px;
  50. padding: 25px;
  51. margin-bottom: 30px;
  52. box-shadow: 0 10px 20px rgba(0,0,0,0.1);
  53. }
  54. .section-header {
  55. border-bottom: 2px solid #eee;
  56. padding-bottom: 15px;
  57. margin-bottom: 20px;
  58. display: flex;
  59. align-items: center;
  60. }
  61. .section-header h2 {
  62. font-size: 1.5em;
  63. color: #2c3e50;
  64. border-left: 5px solid var(--primary-color);
  65. padding-left: 15px;
  66. }
  67. .sub-section {
  68. background: #f8f9fa;
  69. border-radius: 10px;
  70. padding: 20px;
  71. margin-bottom: 20px;
  72. border: 1px solid #e9ecef;
  73. }
  74. .sub-section h3 {
  75. font-size: 1.2em;
  76. margin-bottom: 15px;
  77. color: #555;
  78. display: flex;
  79. align-items: center;
  80. gap: 10px;
  81. }
  82. .control-row {
  83. display: flex;
  84. flex-wrap: wrap;
  85. gap: 15px;
  86. align-items: center;
  87. margin-bottom: 15px;
  88. }
  89. .control-group {
  90. flex: 1;
  91. min-width: 200px;
  92. }
  93. label {
  94. display: block;
  95. margin-bottom: 5px;
  96. font-weight: 600;
  97. font-size: 0.9em;
  98. color: #666;
  99. }
  100. input[type="text"],
  101. input[type="number"],
  102. select {
  103. width: 100%;
  104. padding: 10px;
  105. border: 1px solid #ddd;
  106. border-radius: 6px;
  107. font-size: 14px;
  108. transition: border-color 0.3s;
  109. }
  110. input[type="text"]:focus,
  111. input[type="number"]:focus,
  112. select:focus {
  113. border-color: var(--primary-color);
  114. outline: none;
  115. }
  116. input[type="file"] {
  117. padding: 8px 0;
  118. }
  119. .btn {
  120. padding: 10px 20px;
  121. border: none;
  122. border-radius: 6px;
  123. cursor: pointer;
  124. font-size: 14px;
  125. font-weight: 600;
  126. transition: all 0.2s;
  127. color: white;
  128. text-transform: uppercase;
  129. letter-spacing: 0.5px;
  130. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  131. }
  132. .btn:active {
  133. transform: translateY(1px);
  134. box-shadow: none;
  135. }
  136. .btn-primary { background-color: var(--primary-color); }
  137. .btn-primary:hover { background-color: #3dbdb4; }
  138. .btn-secondary { background-color: var(--secondary-color); }
  139. .btn-secondary:hover { background-color: #ff5252; }
  140. .btn-warning { background-color: var(--accent-color); color: #333; }
  141. .btn-warning:hover { background-color: #fdbf38; }
  142. .btn-info { background-color: #667eea; }
  143. .btn-info:hover { background-color: #5a6fd6; }
  144. .status-display {
  145. background: #e8f5e8;
  146. border: 1px solid #c8e6c9;
  147. color: #2e7d32;
  148. padding: 15px;
  149. border-radius: 8px;
  150. font-size: 0.9em;
  151. margin-top: 15px;
  152. }
  153. .status-display p { margin: 5px 0; }
  154. .message-box {
  155. position: fixed;
  156. top: 20px;
  157. right: 20px;
  158. padding: 15px 25px;
  159. border-radius: 8px;
  160. color: white;
  161. display: none;
  162. z-index: 3000;
  163. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  164. animation: slideIn 0.3s ease-out;
  165. }
  166. @keyframes slideIn {
  167. from { transform: translateX(100%); opacity: 0; }
  168. to { transform: translateX(0); opacity: 1; }
  169. }
  170. .msg-success { background-color: #4caf50; }
  171. .msg-error { background-color: #f44336; }
  172. .loading-overlay {
  173. position: fixed;
  174. top: 0; left: 0; right: 0; bottom: 0;
  175. background: rgba(255,255,255,0.7);
  176. display: none;
  177. justify-content: center;
  178. align-items: center;
  179. z-index: 4000;
  180. font-size: 1.2em;
  181. color: #555;
  182. }
  183. .helper-text {
  184. font-size: 0.85em;
  185. color: #888;
  186. margin-top: 5px;
  187. }
  188. /* 电视网格布局 */
  189. .tv-grid {
  190. display: grid;
  191. grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  192. gap: 20px;
  193. margin-bottom: 20px;
  194. }
  195. .tv-card {
  196. background: white;
  197. border-radius: 12px;
  198. padding: 20px;
  199. text-align: center;
  200. border: 1px solid #eee;
  201. transition: transform 0.2s, box-shadow 0.2s;
  202. cursor: pointer;
  203. display: flex;
  204. flex-direction: column;
  205. align-items: center;
  206. justify-content: center;
  207. }
  208. .tv-card:hover {
  209. transform: translateY(-5px);
  210. box-shadow: 0 10px 20px rgba(0,0,0,0.1);
  211. border-color: var(--primary-color);
  212. }
  213. .tv-icon {
  214. font-size: 3em;
  215. margin-bottom: 10px;
  216. }
  217. .tv-name {
  218. font-weight: bold;
  219. color: #2c3e50;
  220. margin-bottom: 5px;
  221. }
  222. .tv-index {
  223. font-size: 0.8em;
  224. color: #999;
  225. margin-bottom: 10px;
  226. }
  227. /* Modal 弹窗样式 */
  228. .modal {
  229. display: none;
  230. position: fixed;
  231. top: 0; left: 0; width: 100%; height: 100%;
  232. background-color: rgba(0,0,0,0.5);
  233. z-index: 2000;
  234. justify-content: center;
  235. align-items: center;
  236. }
  237. .modal-content {
  238. background-color: white;
  239. padding: 30px;
  240. border-radius: 15px;
  241. width: 90%;
  242. max-width: 500px;
  243. position: relative;
  244. box-shadow: 0 5px 30px rgba(0,0,0,0.3);
  245. max-height: 90vh;
  246. overflow-y: auto;
  247. }
  248. .modal-header {
  249. display: flex;
  250. justify-content: space-between;
  251. align-items: center;
  252. margin-bottom: 20px;
  253. border-bottom: 1px solid #eee;
  254. padding-bottom: 10px;
  255. }
  256. .modal-title {
  257. font-size: 1.4em;
  258. color: #2c3e50;
  259. font-weight: bold;
  260. }
  261. .close-btn {
  262. font-size: 1.5em;
  263. color: #aaa;
  264. cursor: pointer;
  265. line-height: 1;
  266. }
  267. .close-btn:hover {
  268. color: #333;
  269. }
  270. .config-section {
  271. background: #f8f9fa;
  272. padding: 15px;
  273. border-radius: 8px;
  274. margin-bottom: 15px;
  275. border: 1px solid #eee;
  276. }
  277. .config-title {
  278. font-weight: bold;
  279. margin-bottom: 10px;
  280. color: #555;
  281. display: flex;
  282. align-items: center;
  283. gap: 5px;
  284. }
  285. </style>
  286. </head>
  287. <body>
  288. <div class="container">
  289. <div class="header" style="position: relative;">
  290. <h1>展厅控制</h1>
  291. <p>统一管理展厅电视与灯光设备</p>
  292. <a href="/logout" style="position: absolute; right: 20px; top: 20px; text-decoration: none; color: #ff6b6b; font-weight: bold; border: 1px solid #ff6b6b; padding: 5px 15px; border-radius: 5px; transition: all 0.2s;" onmouseover="this.style.background='#ff6b6b';this.style.color='white'" onmouseout="this.style.background='transparent';this.style.color='#ff6b6b'">
  293. 退出登录
  294. </a>
  295. </div>
  296. <!-- 模块一:控制 Kodi 所安装的电视 -->
  297. <div class="main-section">
  298. <div class="section-header">
  299. <h2>📺 电视控制 (Kodi)</h2>
  300. </div>
  301. <!-- 1. 控制单个电视 -->
  302. <div class="sub-section">
  303. <h3>👤 控制单个电视</h3>
  304. <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">点击电视图标进行详细配置</p>
  305. <div id="tv-grid-container" class="tv-grid">
  306. <!-- 动态生成电视图标 -->
  307. <div style="grid-column: 1 / -1; text-align: center; padding: 20px;">
  308. 加载中...
  309. </div>
  310. </div>
  311. </div>
  312. <!-- 2. 控制所有电视 -->
  313. <div class="sub-section">
  314. <h3>👥 控制所有电视</h3>
  315. <div style="background: white; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 15px;">
  316. <h4 style="margin-bottom: 15px; color: #ff6b6b;">同步播放控制</h4>
  317. <div class="control-row">
  318. <div class="control-group">
  319. <label for="videoSelect">选择视频 (Video ID)</label>
  320. <select id="videoSelect">
  321. <option value="">加载中...</option>
  322. </select>
  323. </div>
  324. <div class="control-group" style="flex: 2; display: flex; align-items: flex-end;">
  325. <button class="btn btn-secondary" style="width: 100%;" onclick="startKodi()">同步播放视频</button>
  326. </div>
  327. </div>
  328. <div style="background: white; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 15px;">
  329. <h4 style="margin-bottom: 15px; color: #4ecdc4;">音量控制</h4>
  330. <div class="control-row">
  331. <div class="control-group" style="flex: 1;">
  332. <label for="globalVolume">全局音量 (0-100)</label>
  333. <input type="number" id="globalVolume" min="0" max="100" value="65">
  334. </div>
  335. <div class="control-group" style="flex: 2; display: flex; align-items: flex-end;">
  336. <button class="btn btn-info" style="width: 100%;" onclick="setGlobalVolume()">设置音量</button>
  337. </div>
  338. </div>
  339. </div>
  340. </div>
  341. <div style="background: white; padding: 15px; border-radius: 8px; border: 1px solid #eee;">
  342. <h4 style="margin-bottom: 15px; color: #333;">系统与模式控制</h4>
  343. <div class="control-row" style="gap: 15px;">
  344. <button class="btn btn-primary" style="flex: 1;" onclick="startAllKodiApps()">启动所有电视Kodi应用</button>
  345. <button class="btn btn-warning" style="flex: 1;" onclick="revokeIndividualState()">撤销独立控制 (恢复同步)</button>
  346. </div>
  347. <div class="control-row" style="gap: 15px;">
  348. <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnAllTvs()">唤醒所有电视</button>
  349. <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffAllTvs()">息屏所有电视</button>
  350. </div>
  351. </div>
  352. <div class="status-display" id="kodiStatusDisplay">
  353. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
  354. <strong>📊 Kodi 系统状态</strong>
  355. <button class="btn btn-warning" style="padding: 5px 10px; font-size: 12px;" onclick="getKodiStatus()">刷新</button>
  356. </div>
  357. <div id="kodiStatusContent">加载中...</div>
  358. </div>
  359. </div>
  360. </div>
  361. <!-- 模块三:门禁控制 -->
  362. <div class="main-section">
  363. <div class="section-header">
  364. <h2>🚪 办公楼大门控制</h2>
  365. </div>
  366. <div class="sub-section" style="background: white;">
  367. <!-- 远程开门 -->
  368. <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee; margin-bottom: 15px;">
  369. <h4 style="margin-bottom: 15px; color: #2ecc71;">🔓 远程开门</h4>
  370. <div class="control-row">
  371. <div class="control-group" style="flex: 2;">
  372. <p style="color: #666; margin-bottom: 0; line-height: 38px;">控制办公楼大门 (ID: 1)</p>
  373. </div>
  374. <div class="control-group" style="flex: 0 0 auto; display: flex; align-items: flex-end;">
  375. <button class="btn btn-primary" onclick="remoteOpenDoor()">远程开门</button>
  376. </div>
  377. </div>
  378. </div>
  379. <!-- 模式控制 -->
  380. <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; border: 1px solid #eee;">
  381. <h4 style="margin-bottom: 15px; color: #e74c3c;">⚙️ 模式设置</h4>
  382. <div class="control-row">
  383. <div class="control-group">
  384. <label for="doorControlWay">控制模式</label>
  385. <select id="doorControlWay">
  386. <option value="0">在线 (普通模式)</option>
  387. <option value="1">常开 (保持开启)</option>
  388. <option value="2">常闭 (保持关闭)</option>
  389. </select>
  390. </div>
  391. <div class="control-group" style="flex: 0 0 auto; display: flex; align-items: flex-end;">
  392. <button class="btn btn-warning" onclick="setDoorMode()">应用设置</button>
  393. </div>
  394. </div>
  395. </div>
  396. </div>
  397. </div>
  398. <!-- 模块二:展品灯座控制 -->
  399. <div class="main-section">
  400. <div class="section-header">
  401. <h2>💡 展品灯座控制</h2>
  402. </div>
  403. <div class="sub-section" style="background: white;">
  404. <div class="control-row">
  405. <div class="control-group">
  406. <label for="exhibitId">选择展品</label>
  407. <select id="exhibitId">
  408. {% if led_segments %}
  409. {% for segment in led_segments %}
  410. <option value="{{ segment.id }}">{{ segment.name }} (ID: {{ segment.id }})</option>
  411. {% endfor %}
  412. {% else %}
  413. <option value="0">未找到配置 (默认ID: 0)</option>
  414. {% endif %}
  415. </select>
  416. </div>
  417. <div class="control-group" style="flex: 2; display: flex; align-items: flex-end; gap: 10px;">
  418. <button class="btn btn-primary" onclick="startEffect()">启动灯效</button>
  419. <button class="btn btn-secondary" onclick="stopEffect()">停止灯效</button>
  420. <button class="btn btn-warning" onclick="getStatus()">刷新状态</button>
  421. </div>
  422. </div>
  423. <div class="status-display" id="ledStatusDisplay">
  424. <div id="statusContent">点击"刷新状态"查看当前LED状态</div>
  425. </div>
  426. <div style="margin-top: 20px; padding: 15px; background: #f0f8ff; border-radius: 8px; border-left: 4px solid #4ecdc4;">
  427. <p><strong>📖 灯效说明:</strong> 指定展品将显示白色呼吸灯效,其他展品保持静止。10秒后,所有展品将随机播放波浪、闪烁或呼吸效果。</p>
  428. </div>
  429. </div>
  430. </div>
  431. </div>
  432. <!-- 弹窗 (Modal) -->
  433. <div id="tvConfigModal" class="modal">
  434. <div class="modal-content">
  435. <div class="modal-header">
  436. <div class="modal-title">
  437. 📺 <span id="modalTvName">电视配置</span>
  438. </div>
  439. <span class="close-btn" onclick="closeTvModal()">&times;</span>
  440. </div>
  441. <!-- 电源控制 -->
  442. <div class="config-section">
  443. <div class="config-title" style="color: #ff6b6b;">🔌 电源控制</div>
  444. <div class="control-row" style="margin-bottom: 0;">
  445. <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnCurrentTv()">唤醒</button>
  446. <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffCurrentTv()">息屏</button>
  447. </div>
  448. </div>
  449. <!-- 图片播放 -->
  450. <div class="config-section">
  451. <div class="config-title" style="color: #4ecdc4;">🖼️ 图片播放</div>
  452. <div class="control-group" style="margin-bottom: 10px;">
  453. <label>方式一:上传图片</label>
  454. <input type="file" id="modalImageFile" accept="image/*">
  455. </div>
  456. <div class="control-group" style="margin-bottom: 10px;">
  457. <label>方式二:图片URL</label>
  458. <input type="text" id="modalImageUrl" placeholder="http://example.com/image.jpg">
  459. </div>
  460. <button class="btn btn-primary" style="width: 100%;" onclick="playImageCurrentTv()">播放图片</button>
  461. </div>
  462. <!-- RTSP播放 -->
  463. <div class="config-section">
  464. <div class="config-title" style="color: #667eea;">📹 RTSP视频流</div>
  465. <div class="control-group" style="margin-bottom: 10px;">
  466. <label>RTSP URL</label>
  467. <input type="text" id="modalRtspUrl" placeholder="rtsp://example.com/stream">
  468. </div>
  469. <div class="control-group" style="margin-bottom: 10px;">
  470. <label>音量 (0-100)</label>
  471. <input type="number" id="modalRtspVolume" min="0" max="100" value="0">
  472. </div>
  473. <button class="btn btn-info" style="width: 100%;" onclick="playRtspCurrentTv()">播放RTSP流</button>
  474. </div>
  475. </div>
  476. </div>
  477. <!-- 消息提示与遮罩 -->
  478. <div id="messageBox" class="message-box"></div>
  479. <div id="loadingOverlay" class="loading-overlay">
  480. <div><span style="font-size: 2em;">⏳</span><br>处理中...</div>
  481. </div>
  482. <script>
  483. // 全局变量
  484. let currentStatus = null;
  485. let currentKodiStatus = null;
  486. let activeTvIndex = null; // 当前正在配置的电视Index
  487. document.addEventListener('DOMContentLoaded', function() {
  488. getStatus();
  489. getKodiStatus();
  490. getKodiClients(); // 加载Kodi客户端列表并生成卡片
  491. getVideoList(); // 加载视频列表
  492. // 点击Modal外部关闭
  493. window.onclick = function(event) {
  494. const modal = document.getElementById('tvConfigModal');
  495. if (event.target == modal) {
  496. closeTvModal();
  497. }
  498. }
  499. });
  500. // 消息提示函数
  501. function showMessage(message, type = 'success') {
  502. const msgBox = document.getElementById('messageBox');
  503. msgBox.textContent = message;
  504. msgBox.className = 'message-box ' + (type === 'success' ? 'msg-success' : 'msg-error');
  505. msgBox.style.display = 'block';
  506. // 重新触发动画
  507. msgBox.style.animation = 'none';
  508. msgBox.offsetHeight; /* trigger reflow */
  509. msgBox.style.animation = null;
  510. setTimeout(() => {
  511. msgBox.style.display = 'none';
  512. }, 3000);
  513. }
  514. function showLoading(show = true) {
  515. document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
  516. }
  517. // ===== 展品灯效控制 =====
  518. async function getStatus() {
  519. try {
  520. showLoading(true);
  521. const response = await fetch('/api/led/status');
  522. const result = await response.json();
  523. if (result.success) {
  524. currentStatus = result.data;
  525. document.getElementById('statusContent').innerHTML = `
  526. <p><strong>运行状态:</strong> ${currentStatus.is_running ? '运行中' : '已停止'}</p>
  527. <p><strong>信息:</strong> ${currentStatus.message}</p>
  528. `;
  529. showMessage('LED状态已刷新');
  530. } else {
  531. showMessage('获取LED状态失败: ' + result.message, 'error');
  532. }
  533. } catch (error) {
  534. showMessage('网络错误: ' + error.message, 'error');
  535. } finally {
  536. showLoading(false);
  537. }
  538. }
  539. async function startEffect() {
  540. const exhibitId = parseInt(document.getElementById('exhibitId').value);
  541. if (isNaN(exhibitId) || exhibitId < 0) {
  542. showMessage('请输入有效的展品ID', 'error');
  543. return;
  544. }
  545. try {
  546. showLoading(true);
  547. const response = await fetch('/api/led/start', {
  548. method: 'POST',
  549. headers: { 'Content-Type': 'application/json' },
  550. body: JSON.stringify({ exhibit_id: exhibitId })
  551. });
  552. const result = await response.json();
  553. if (result.success) {
  554. showMessage(result.message);
  555. getStatus();
  556. } else {
  557. showMessage(result.message, 'error');
  558. }
  559. } catch (error) {
  560. showMessage('网络错误: ' + error.message, 'error');
  561. } finally {
  562. showLoading(false);
  563. }
  564. }
  565. async function stopEffect() {
  566. try {
  567. showLoading(true);
  568. const response = await fetch('/api/led/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
  569. const result = await response.json();
  570. if (result.success) {
  571. showMessage(result.message);
  572. getStatus();
  573. } else {
  574. showMessage(result.message, 'error');
  575. }
  576. } catch (error) {
  577. showMessage('网络错误: ' + error.message, 'error');
  578. } finally {
  579. showLoading(false);
  580. }
  581. }
  582. // ===== Kodi 控制 =====
  583. async function getKodiStatus() {
  584. try {
  585. // showLoading(true); // 状态刷新不强制显示全屏遮罩,体验更好
  586. const response = await fetch('/api/kodi/status');
  587. const result = await response.json();
  588. if (result.success) {
  589. currentKodiStatus = result.data;
  590. document.getElementById('kodiStatusContent').innerHTML = `
  591. <p><strong>线程状态:</strong> ${currentKodiStatus.is_running ? '运行中' : '已停止'}</p>
  592. <p><strong>信息:</strong> ${currentKodiStatus.message}</p>
  593. `;
  594. } else {
  595. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">获取失败: ${result.message}</span>`;
  596. }
  597. } catch (error) {
  598. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">网络错误</span>`;
  599. }
  600. }
  601. async function getKodiClients() {
  602. const container = document.getElementById('tv-grid-container');
  603. if (!container) return;
  604. try {
  605. const response = await fetch('/api/kodi/clients');
  606. const result = await response.json();
  607. if (result.success && result.data.length > 0) {
  608. container.innerHTML = ''; // 清空
  609. result.data.forEach(client => {
  610. const index = client.index;
  611. const name = client.name;
  612. // 生成电视大图标卡片
  613. const cardDiv = document.createElement('div');
  614. cardDiv.className = 'tv-card';
  615. cardDiv.onclick = () => openTvModal(index, name);
  616. cardDiv.innerHTML = `
  617. <div class="tv-icon">📺</div>
  618. <div class="tv-name">${name}</div>
  619. <div class="tv-index">ID: ${index}</div>
  620. <button class="btn btn-primary" style="padding: 5px 15px; font-size: 12px; margin-top: 5px;">
  621. ⚙️ 配置
  622. </button>
  623. `;
  624. container.appendChild(cardDiv);
  625. });
  626. } else {
  627. container.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; color: #666;">未找到 Kodi 客户端</div>';
  628. showMessage('未找到 Kodi 客户端', 'error');
  629. }
  630. } catch (error) {
  631. console.error(error);
  632. container.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; color: red;">加载失败</div>';
  633. showMessage('加载 Kodi 客户端列表失败', 'error');
  634. }
  635. }
  636. async function getVideoList() {
  637. const selectEl = document.getElementById('videoSelect');
  638. try {
  639. const response = await fetch('/api/kodi/videos');
  640. const result = await response.json();
  641. if (result.success && result.data.length > 0) {
  642. selectEl.innerHTML = ''; // 清空
  643. result.data.forEach(video => {
  644. const option = document.createElement('option');
  645. option.value = video.id;
  646. option.textContent = `${video.name} (ID: ${video.id})`;
  647. selectEl.appendChild(option);
  648. });
  649. } else {
  650. selectEl.innerHTML = '<option value="">未找到视频</option>';
  651. showMessage('未找到视频列表', 'error');
  652. }
  653. } catch (error) {
  654. console.error(error);
  655. selectEl.innerHTML = '<option value="">加载失败</option>';
  656. showMessage('加载视频列表失败', 'error');
  657. }
  658. }
  659. async function startKodi() {
  660. const videoId = parseInt(document.getElementById('videoSelect').value);
  661. // const volume = parseInt(document.getElementById('syncVolume').value); // 移除了同步播放时的音量设置
  662. if (isNaN(videoId) || videoId < 0) {
  663. showMessage('请选择有效的视频', 'error');
  664. return;
  665. }
  666. // if (isNaN(volume) || volume < 0 || volume > 100) {
  667. // showMessage('音量必须是 0-100 之间的整数', 'error');
  668. // return;
  669. // }
  670. try {
  671. showLoading(true);
  672. const response = await fetch('/api/kodi/start', {
  673. method: 'POST',
  674. headers: { 'Content-Type': 'application/json' },
  675. body: JSON.stringify({ video_id: videoId }) // 移除了 volume
  676. });
  677. const result = await response.json();
  678. if (result.success) {
  679. showMessage(result.message);
  680. getKodiStatus();
  681. } else {
  682. showMessage(result.message, 'error');
  683. }
  684. } catch (error) {
  685. showMessage('网络错误: ' + error.message, 'error');
  686. } finally {
  687. showLoading(false);
  688. }
  689. }
  690. async function setGlobalVolume() {
  691. const volume = parseInt(document.getElementById('globalVolume').value);
  692. if (isNaN(volume) || volume < 0 || volume > 100) {
  693. showMessage('音量必须是 0-100 之间的整数', 'error');
  694. return;
  695. }
  696. try {
  697. showLoading(true);
  698. const response = await fetch('/api/kodi/set_volume', {
  699. method: 'POST',
  700. headers: { 'Content-Type': 'application/json' },
  701. body: JSON.stringify({ volume: volume })
  702. });
  703. const result = await response.json();
  704. if (result.success) {
  705. showMessage(result.message);
  706. } else {
  707. showMessage(result.message, 'error');
  708. }
  709. } catch (error) {
  710. showMessage('网络错误: ' + error.message, 'error');
  711. } finally {
  712. showLoading(false);
  713. }
  714. }
  715. async function revokeIndividualState() {
  716. try {
  717. showLoading(true);
  718. const response = await fetch('/api/kodi/revoke_individual_state', {
  719. method: 'POST',
  720. headers: { 'Content-Type': 'application/json' }
  721. });
  722. const result = await response.json();
  723. if (result.success) {
  724. showMessage(result.message);
  725. getKodiStatus();
  726. } else {
  727. showMessage(result.message, 'error');
  728. }
  729. } catch (error) {
  730. showMessage('网络错误: ' + error.message, 'error');
  731. } finally {
  732. showLoading(false);
  733. }
  734. }
  735. async function startAllKodiApps() {
  736. try {
  737. showLoading(true);
  738. const response = await fetch('/api/kodi/start_all_apps', {
  739. method: 'POST',
  740. headers: { 'Content-Type': 'application/json' }
  741. });
  742. const result = await response.json();
  743. if (result.success) {
  744. showMessage(result.message);
  745. getKodiStatus();
  746. } else {
  747. showMessage(result.message, 'error');
  748. }
  749. } catch (error) {
  750. showMessage('网络错误: ' + error.message, 'error');
  751. } finally {
  752. showLoading(false);
  753. }
  754. }
  755. // ===== Modal 弹窗控制逻辑 =====
  756. function openTvModal(index, name) {
  757. activeTvIndex = index;
  758. document.getElementById('modalTvName').textContent = `${name} (Index: ${index})`;
  759. // 重置输入框
  760. document.getElementById('modalImageFile').value = '';
  761. document.getElementById('modalImageUrl').value = '';
  762. document.getElementById('modalRtspUrl').value = '';
  763. document.getElementById('modalRtspVolume').value = '0';
  764. document.getElementById('tvConfigModal').style.display = 'flex';
  765. }
  766. function closeTvModal() {
  767. document.getElementById('tvConfigModal').style.display = 'none';
  768. activeTvIndex = null;
  769. }
  770. // ===== 电视电源控制 (单台 - Modal内) =====
  771. async function turnOnCurrentTv() {
  772. if (activeTvIndex === null) return;
  773. try {
  774. showLoading(true);
  775. const response = await fetch('/api/mitv/turn_on', {
  776. method: 'POST',
  777. headers: { 'Content-Type': 'application/json' },
  778. body: JSON.stringify({ kodi_id: activeTvIndex })
  779. });
  780. const result = await response.json();
  781. if (result.success) {
  782. showMessage(result.message);
  783. } else {
  784. showMessage(result.message, 'error');
  785. }
  786. } catch (error) {
  787. showMessage('网络错误: ' + error.message, 'error');
  788. } finally {
  789. showLoading(false);
  790. }
  791. }
  792. async function turnOffCurrentTv() {
  793. if (activeTvIndex === null) return;
  794. try {
  795. showLoading(true);
  796. const response = await fetch('/api/mitv/turn_off', {
  797. method: 'POST',
  798. headers: { 'Content-Type': 'application/json' },
  799. body: JSON.stringify({ kodi_id: activeTvIndex })
  800. });
  801. const result = await response.json();
  802. if (result.success) {
  803. showMessage(result.message);
  804. } else {
  805. showMessage(result.message, 'error');
  806. }
  807. } catch (error) {
  808. showMessage('网络错误: ' + error.message, 'error');
  809. } finally {
  810. showLoading(false);
  811. }
  812. }
  813. // ===== 电视控制 (图片/RTSP - Modal内) =====
  814. async function playImageCurrentTv() {
  815. if (activeTvIndex === null) return;
  816. const fileInput = document.getElementById('modalImageFile');
  817. const urlInput = document.getElementById('modalImageUrl').value.trim();
  818. if ((!fileInput.files || fileInput.files.length === 0) && !urlInput) {
  819. showMessage('请选择图片文件或输入图片URL', 'error');
  820. return;
  821. }
  822. try {
  823. showLoading(true);
  824. let response;
  825. if (fileInput.files && fileInput.files.length > 0) {
  826. const formData = new FormData();
  827. formData.append('file', fileInput.files[0]);
  828. formData.append('kodi_client_index', activeTvIndex);
  829. response = await fetch('/api/kodi/play_image', {
  830. method: 'POST',
  831. body: formData
  832. });
  833. } else {
  834. response = await fetch('/api/kodi/play_image', {
  835. method: 'POST',
  836. headers: { 'Content-Type': 'application/json' },
  837. body: JSON.stringify({
  838. image_url: urlInput,
  839. kodi_client_index: activeTvIndex
  840. })
  841. });
  842. }
  843. const result = await response.json();
  844. if (result.success) {
  845. showMessage(result.message);
  846. // 关闭弹窗可能比较好,也可能用户想继续操作,暂时不关
  847. } else {
  848. showMessage(result.message, 'error');
  849. }
  850. } catch (error) {
  851. showMessage('网络错误: ' + error.message, 'error');
  852. } finally {
  853. showLoading(false);
  854. }
  855. }
  856. async function playRtspCurrentTv() {
  857. if (activeTvIndex === null) return;
  858. const rtspUrl = document.getElementById('modalRtspUrl').value.trim();
  859. const volume = parseInt(document.getElementById('modalRtspVolume').value);
  860. if (!rtspUrl) {
  861. showMessage('请输入RTSP视频流URL', 'error');
  862. return;
  863. }
  864. if (isNaN(volume) || volume < 0 || volume > 100) {
  865. showMessage('音量必须是0-100之间的整数', 'error');
  866. return;
  867. }
  868. try {
  869. showLoading(true);
  870. const response = await fetch('/api/kodi/play_rtsp', {
  871. method: 'POST',
  872. headers: { 'Content-Type': 'application/json' },
  873. body: JSON.stringify({
  874. rtsp_url: rtspUrl,
  875. kodi_client_index: activeTvIndex,
  876. volume: volume
  877. })
  878. });
  879. const result = await response.json();
  880. if (result.success) {
  881. showMessage(result.message);
  882. } else {
  883. showMessage(result.message, 'error');
  884. }
  885. } catch (error) {
  886. showMessage('网络错误: ' + error.message, 'error');
  887. } finally {
  888. showLoading(false);
  889. }
  890. }
  891. // ===== 全局电视电源控制 =====
  892. async function turnOnAllTvs() {
  893. if (!confirm('确定要唤醒所有电视吗?')) return;
  894. try {
  895. showLoading(true);
  896. const response = await fetch('/api/mitv/turn_on_all', {
  897. method: 'POST',
  898. headers: { 'Content-Type': 'application/json' }
  899. });
  900. const result = await response.json();
  901. if (result.success) {
  902. showMessage(result.message);
  903. } else {
  904. showMessage(result.message, 'error');
  905. }
  906. } catch (error) {
  907. showMessage('网络错误: ' + error.message, 'error');
  908. } finally {
  909. showLoading(false);
  910. }
  911. }
  912. async function turnOffAllTvs() {
  913. if (!confirm('确定要息屏所有电视吗?')) return;
  914. try {
  915. showLoading(true);
  916. const response = await fetch('/api/mitv/turn_off_all', {
  917. method: 'POST',
  918. headers: { 'Content-Type': 'application/json' }
  919. });
  920. const result = await response.json();
  921. if (result.success) {
  922. showMessage(result.message);
  923. } else {
  924. showMessage(result.message, 'error');
  925. }
  926. } catch (error) {
  927. showMessage('网络错误: ' + error.message, 'error');
  928. } finally {
  929. showLoading(false);
  930. }
  931. }
  932. // ===== 门禁控制 =====
  933. async function remoteOpenDoor() {
  934. const doorId = 1; // 默认ID为1
  935. const payload = { door_id: doorId };
  936. try {
  937. showLoading(true);
  938. const response = await fetch('/api/door/open', {
  939. method: 'POST',
  940. headers: { 'Content-Type': 'application/json' },
  941. body: JSON.stringify(payload)
  942. });
  943. const result = await response.json();
  944. if (result.success) {
  945. showMessage(result.message);
  946. } else {
  947. showMessage(result.message, 'error');
  948. }
  949. } catch (error) {
  950. showMessage('网络错误: ' + error.message, 'error');
  951. } finally {
  952. showLoading(false);
  953. }
  954. }
  955. async function setDoorMode() {
  956. const controlWay = parseInt(document.getElementById('doorControlWay').value);
  957. const payload = { control_way: controlWay };
  958. const modeName = controlWay === 0 ? '在线' : (controlWay === 1 ? '常开' : '常闭');
  959. if (!confirm(`确定要将门禁设置为"${modeName}"模式吗?`)) return;
  960. try {
  961. showLoading(true);
  962. const response = await fetch('/api/door/control', {
  963. method: 'POST',
  964. headers: { 'Content-Type': 'application/json' },
  965. body: JSON.stringify(payload)
  966. });
  967. const result = await response.json();
  968. if (result.success) {
  969. showMessage(result.message);
  970. } else {
  971. showMessage(result.message, 'error');
  972. }
  973. } catch (error) {
  974. showMessage('网络错误: ' + error.message, 'error');
  975. } finally {
  976. showLoading(false);
  977. }
  978. }
  979. </script>
  980. </body>
  981. </html>