index.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. {% extends "base.html" %}
  2. {% block title %}电视控制{% endblock %}
  3. {% block extra_styles %}
  4. .tv-grid {
  5. display: flex;
  6. flex-wrap: wrap;
  7. gap: 15px;
  8. margin-bottom: 20px;
  9. justify-content: flex-start;
  10. }
  11. .tv-card {
  12. flex: 0 0 auto;
  13. width: 140px;
  14. background: #f8f9fa;
  15. border-radius: 12px;
  16. padding: 15px;
  17. text-align: center;
  18. border: 1px solid #eee;
  19. transition: all 0.2s;
  20. cursor: pointer;
  21. display: flex;
  22. flex-direction: column;
  23. align-items: center;
  24. justify-content: center;
  25. }
  26. .tv-card:hover {
  27. transform: translateY(-5px);
  28. box-shadow: 0 8px 15px rgba(0,0,0,0.1);
  29. background: white;
  30. border-color: var(--primary-color);
  31. }
  32. .tv-icon { font-size: 3em; margin-bottom: 10px; }
  33. .tv-name { font-weight: bold; color: #2c3e50; margin-bottom: 5px; }
  34. .tv-index { font-size: 0.8em; color: #999; margin-bottom: 10px; }
  35. /* Modal */
  36. .modal {
  37. display: none;
  38. position: fixed;
  39. top: 0; left: 0; width: 100%; height: 100%;
  40. background-color: rgba(0,0,0,0.5);
  41. z-index: 2000;
  42. justify-content: center;
  43. align-items: center;
  44. }
  45. .modal-content {
  46. background-color: white;
  47. padding: 30px;
  48. border-radius: 15px;
  49. width: 90%;
  50. max-width: 500px;
  51. max-height: 90vh;
  52. overflow-y: auto;
  53. position: relative;
  54. box-shadow: 0 5px 30px rgba(0,0,0,0.3);
  55. }
  56. .modal-header {
  57. display: flex;
  58. justify-content: space-between;
  59. align-items: center;
  60. margin-bottom: 20px;
  61. border-bottom: 1px solid #eee;
  62. padding-bottom: 10px;
  63. }
  64. .modal-title { font-size: 1.4em; color: #2c3e50; font-weight: bold; }
  65. .close-btn { font-size: 1.5em; cursor: pointer; color: #aaa; line-height: 1; }
  66. .close-btn:hover { color: #333; }
  67. .config-section {
  68. background: #f8f9fa;
  69. padding: 15px;
  70. border-radius: 8px;
  71. margin-bottom: 15px;
  72. border: 1px solid #eee;
  73. }
  74. {% endblock %}
  75. {% block content %}
  76. <div class="module-header">
  77. <h2>📺 电视控制 (Kodi)</h2>
  78. </div>
  79. <div class="sub-section">
  80. <h3>👤 控制单个电视</h3>
  81. <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">点击电视图标进行详细配置</p>
  82. <div id="tv-grid-container" class="tv-grid">
  83. <div style="width: 100%; text-align: center; padding: 20px;">
  84. 加载中...
  85. </div>
  86. </div>
  87. </div>
  88. <div class="sub-section">
  89. <h3>👥 控制所有电视</h3>
  90. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-bottom: 20px;">
  91. <h4 style="margin-bottom: 15px; color: #ff6b6b; border-left: 4px solid #ff6b6b; padding-left: 10px;">同步播放控制</h4>
  92. <div class="control-row">
  93. <div class="control-group">
  94. <label for="videoSelect">选择视频 (Video ID)</label>
  95. <select id="videoSelect">
  96. <option value="">加载中...</option>
  97. </select>
  98. </div>
  99. <div class="control-group" style="flex: 0 0 auto;">
  100. <button class="btn btn-secondary" onclick="startKodi()">同步播放视频</button>
  101. </div>
  102. </div>
  103. </div>
  104. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-bottom: 20px;">
  105. <h4 style="margin-bottom: 15px; color: #4ecdc4; border-left: 4px solid #4ecdc4; padding-left: 10px;">音量控制</h4>
  106. <div class="control-row">
  107. <div class="control-group">
  108. <label for="globalVolume">全局音量 (0-100)</label>
  109. <input type="number" id="globalVolume" min="0" max="100" value="65">
  110. </div>
  111. <div class="control-group" style="flex: 0 0 auto;">
  112. <button class="btn btn-info" onclick="setGlobalVolume()">设置音量</button>
  113. </div>
  114. </div>
  115. </div>
  116. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee;">
  117. <h4 style="margin-bottom: 15px; color: #333; border-left: 4px solid #333; padding-left: 10px;">系统与模式控制</h4>
  118. <div class="control-row">
  119. <button class="btn btn-primary" style="flex: 1;" onclick="startAllKodiApps()">启动所有电视Kodi应用</button>
  120. <button class="btn btn-warning" style="flex: 1;" onclick="revokeIndividualState()">撤销独立控制 (恢复同步)</button>
  121. </div>
  122. <div class="control-row">
  123. <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnAllTvs()">唤醒所有电视</button>
  124. <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffAllTvs()">息屏所有电视</button>
  125. </div>
  126. </div>
  127. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-top: 20px;">
  128. <h4 style="margin-bottom: 15px; color: #9b59b6; border-left: 4px solid #9b59b6; padding-left: 10px;">⏰ 定时闲时播放控制</h4>
  129. <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">
  130. 启用后,系统将在 07:30 - 18:00 期间自动循环播放视频。
  131. </p>
  132. <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
  133. <div>
  134. <strong>当前状态:</strong>
  135. <span id="freeTimeStatusText" style="font-weight: bold; color: #666;">加载中...</span>
  136. </div>
  137. <div class="control-row" style="margin-bottom: 0; gap: 10px; flex: 0 0 auto;">
  138. <button id="btnEnableFreeTime" class="btn btn-primary" onclick="controlFreeTimePlay('start')">开启功能</button>
  139. <button id="btnDisableFreeTime" class="btn btn-secondary" style="background-color: #95a5a6; display: none;" onclick="controlFreeTimePlay('stop')">关闭功能</button>
  140. </div>
  141. </div>
  142. </div>
  143. <div class="status-display" id="kodiStatusDisplay">
  144. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
  145. <strong>📊 Kodi 系统状态</strong>
  146. <button class="btn btn-warning" style="padding: 5px 10px; font-size: 12px;" onclick="getKodiStatus()">刷新</button>
  147. </div>
  148. <div id="kodiStatusContent">加载中...</div>
  149. </div>
  150. </div>
  151. <!-- 弹窗 (Modal) -->
  152. <div id="tvConfigModal" class="modal">
  153. <div class="modal-content">
  154. <div class="modal-header">
  155. <div class="modal-title">
  156. 📺 <span id="modalTvName">电视配置</span>
  157. </div>
  158. <span class="close-btn" onclick="closeTvModal()">&times;</span>
  159. </div>
  160. <!-- 电源控制 -->
  161. <div class="config-section">
  162. <div class="config-title" style="color: #ff6b6b; font-weight: bold; margin-bottom: 10px;">🔌 电源控制</div>
  163. <div class="control-row" style="margin-bottom: 0;">
  164. <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnCurrentTv()">唤醒</button>
  165. <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffCurrentTv()">息屏</button>
  166. </div>
  167. </div>
  168. <!-- 图片播放 -->
  169. <div class="config-section">
  170. <div class="config-title" style="color: #4ecdc4; font-weight: bold; margin-bottom: 10px;">🖼️ 图片播放</div>
  171. <div class="control-group" style="margin-bottom: 10px;">
  172. <label>方式一:上传图片</label>
  173. <input type="file" id="modalImageFile" accept="image/*">
  174. </div>
  175. <div class="control-group" style="margin-bottom: 10px;">
  176. <label>方式二:图片URL</label>
  177. <input type="text" id="modalImageUrl" placeholder="http://example.com/image.jpg">
  178. </div>
  179. <button class="btn btn-primary" style="width: 100%;" onclick="playImageCurrentTv()">播放图片</button>
  180. </div>
  181. <!-- RTSP播放 -->
  182. <div class="config-section">
  183. <div class="config-title" style="color: #667eea; font-weight: bold; margin-bottom: 10px;">📹 RTSP视频流</div>
  184. <div class="control-group" style="margin-bottom: 10px;">
  185. <label>RTSP URL</label>
  186. <input type="text" id="modalRtspUrl" placeholder="rtsp://example.com/stream">
  187. </div>
  188. <div class="control-group" style="margin-bottom: 10px;">
  189. <label>音量 (0-100)</label>
  190. <input type="number" id="modalRtspVolume" min="0" max="100" value="0">
  191. </div>
  192. <button class="btn btn-info" style="width: 100%;" onclick="playRtspCurrentTv()">播放RTSP流</button>
  193. </div>
  194. </div>
  195. </div>
  196. {% endblock %}
  197. {% block scripts %}
  198. <script>
  199. let currentKodiStatus = null;
  200. let activeTvIndex = null;
  201. document.addEventListener('DOMContentLoaded', function() {
  202. getKodiStatus();
  203. getKodiClients();
  204. getVideoList();
  205. getFreeTimePlayStatus();
  206. window.onclick = function(event) {
  207. const modal = document.getElementById('tvConfigModal');
  208. if (event.target == modal) {
  209. closeTvModal();
  210. }
  211. }
  212. });
  213. async function getKodiStatus() {
  214. try {
  215. const response = await fetch('/api/kodi/status');
  216. const result = await response.json();
  217. if (result.success) {
  218. currentKodiStatus = result.data;
  219. document.getElementById('kodiStatusContent').innerHTML = `
  220. <p><strong>线程状态:</strong> ${currentKodiStatus.is_running ? '运行中' : '已停止'}</p>
  221. <p><strong>信息:</strong> ${currentKodiStatus.message}</p>
  222. `;
  223. } else {
  224. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">获取失败: ${result.message}</span>`;
  225. }
  226. } catch (error) {
  227. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">网络错误</span>`;
  228. }
  229. }
  230. async function getKodiClients() {
  231. const container = document.getElementById('tv-grid-container');
  232. if (!container) return;
  233. try {
  234. const response = await fetch('/api/kodi/clients');
  235. const result = await response.json();
  236. if (result.success && result.data.length > 0) {
  237. container.innerHTML = '';
  238. result.data.forEach(client => {
  239. const index = client.index;
  240. const name = client.name;
  241. const cardDiv = document.createElement('div');
  242. cardDiv.className = 'tv-card';
  243. cardDiv.onclick = () => openTvModal(index, name);
  244. cardDiv.innerHTML = `
  245. <div class="tv-icon">📺</div>
  246. <div class="tv-name">${name}</div>
  247. <div class="tv-index">ID: ${index}</div>
  248. <button class="btn btn-primary" style="padding: 5px 15px; font-size: 12px; margin-top: 5px;">
  249. ⚙️ 配置
  250. </button>
  251. `;
  252. container.appendChild(cardDiv);
  253. });
  254. } else {
  255. container.innerHTML = '<div style="width: 100%; text-align: center; color: #666;">未找到 Kodi 客户端</div>';
  256. }
  257. } catch (error) {
  258. console.error(error);
  259. container.innerHTML = '<div style="width: 100%; text-align: center; color: red;">加载失败</div>';
  260. }
  261. }
  262. async function getVideoList() {
  263. const selectEl = document.getElementById('videoSelect');
  264. try {
  265. const response = await fetch('/api/kodi/videos');
  266. const result = await response.json();
  267. if (result.success && result.data.length > 0) {
  268. selectEl.innerHTML = '';
  269. result.data.forEach(video => {
  270. const option = document.createElement('option');
  271. option.value = video.id;
  272. option.textContent = `${video.name} (ID: ${video.id})`;
  273. selectEl.appendChild(option);
  274. });
  275. } else {
  276. selectEl.innerHTML = '<option value="">未找到视频</option>';
  277. }
  278. } catch (error) {
  279. console.error(error);
  280. selectEl.innerHTML = '<option value="">加载失败</option>';
  281. }
  282. }
  283. async function startKodi() {
  284. const videoId = parseInt(document.getElementById('videoSelect').value);
  285. if (isNaN(videoId) || videoId < 0) {
  286. showMessage('请选择有效的视频', 'error');
  287. return;
  288. }
  289. try {
  290. showLoading(true);
  291. const response = await fetch('/api/kodi/start', {
  292. method: 'POST',
  293. headers: { 'Content-Type': 'application/json' },
  294. body: JSON.stringify({ video_id: videoId })
  295. });
  296. const result = await response.json();
  297. if (result.success) {
  298. showMessage(result.message);
  299. getKodiStatus();
  300. } else {
  301. showMessage(result.message, 'error');
  302. }
  303. } catch (error) {
  304. showMessage('网络错误: ' + error.message, 'error');
  305. } finally {
  306. showLoading(false);
  307. }
  308. }
  309. async function setGlobalVolume() {
  310. const volume = parseInt(document.getElementById('globalVolume').value);
  311. if (isNaN(volume) || volume < 0 || volume > 100) {
  312. showMessage('音量必须是 0-100 之间的整数', 'error');
  313. return;
  314. }
  315. try {
  316. showLoading(true);
  317. const response = await fetch('/api/kodi/set_volume', {
  318. method: 'POST',
  319. headers: { 'Content-Type': 'application/json' },
  320. body: JSON.stringify({ volume: volume })
  321. });
  322. const result = await response.json();
  323. if (result.success) {
  324. showMessage(result.message);
  325. } else {
  326. showMessage(result.message, 'error');
  327. }
  328. } catch (error) {
  329. showMessage('网络错误: ' + error.message, 'error');
  330. } finally {
  331. showLoading(false);
  332. }
  333. }
  334. async function revokeIndividualState() {
  335. try {
  336. showLoading(true);
  337. const response = await fetch('/api/kodi/revoke_individual_state', {
  338. method: 'POST',
  339. headers: { 'Content-Type': 'application/json' }
  340. });
  341. const result = await response.json();
  342. if (result.success) {
  343. showMessage(result.message);
  344. getKodiStatus();
  345. } else {
  346. showMessage(result.message, 'error');
  347. }
  348. } catch (error) {
  349. showMessage('网络错误: ' + error.message, 'error');
  350. } finally {
  351. showLoading(false);
  352. }
  353. }
  354. async function startAllKodiApps() {
  355. try {
  356. showLoading(true);
  357. const response = await fetch('/api/kodi/start_all_apps', {
  358. method: 'POST',
  359. headers: { 'Content-Type': 'application/json' }
  360. });
  361. const result = await response.json();
  362. if (result.success) {
  363. showMessage(result.message);
  364. getKodiStatus();
  365. } else {
  366. showMessage(result.message, 'error');
  367. }
  368. } catch (error) {
  369. showMessage('网络错误: ' + error.message, 'error');
  370. } finally {
  371. showLoading(false);
  372. }
  373. }
  374. function openTvModal(index, name) {
  375. activeTvIndex = index;
  376. document.getElementById('modalTvName').textContent = `${name} (Index: ${index})`;
  377. document.getElementById('modalImageFile').value = '';
  378. document.getElementById('modalImageUrl').value = '';
  379. document.getElementById('modalRtspUrl').value = '';
  380. document.getElementById('modalRtspVolume').value = '0';
  381. document.getElementById('tvConfigModal').style.display = 'flex';
  382. }
  383. function closeTvModal() {
  384. document.getElementById('tvConfigModal').style.display = 'none';
  385. activeTvIndex = null;
  386. }
  387. async function turnOnCurrentTv() {
  388. if (activeTvIndex === null) return;
  389. try {
  390. showLoading(true);
  391. const response = await fetch('/api/mitv/turn_on', {
  392. method: 'POST',
  393. headers: { 'Content-Type': 'application/json' },
  394. body: JSON.stringify({ kodi_id: activeTvIndex })
  395. });
  396. const result = await response.json();
  397. if (result.success) {
  398. showMessage(result.message);
  399. } else {
  400. showMessage(result.message, 'error');
  401. }
  402. } catch (error) {
  403. showMessage('网络错误: ' + error.message, 'error');
  404. } finally {
  405. showLoading(false);
  406. }
  407. }
  408. async function turnOffCurrentTv() {
  409. if (activeTvIndex === null) return;
  410. try {
  411. showLoading(true);
  412. const response = await fetch('/api/mitv/turn_off', {
  413. method: 'POST',
  414. headers: { 'Content-Type': 'application/json' },
  415. body: JSON.stringify({ kodi_id: activeTvIndex })
  416. });
  417. const result = await response.json();
  418. if (result.success) {
  419. showMessage(result.message);
  420. } else {
  421. showMessage(result.message, 'error');
  422. }
  423. } catch (error) {
  424. showMessage('网络错误: ' + error.message, 'error');
  425. } finally {
  426. showLoading(false);
  427. }
  428. }
  429. async function playImageCurrentTv() {
  430. if (activeTvIndex === null) return;
  431. const fileInput = document.getElementById('modalImageFile');
  432. const urlInput = document.getElementById('modalImageUrl').value.trim();
  433. if ((!fileInput.files || fileInput.files.length === 0) && !urlInput) {
  434. showMessage('请选择图片文件或输入图片URL', 'error');
  435. return;
  436. }
  437. try {
  438. showLoading(true);
  439. let response;
  440. if (fileInput.files && fileInput.files.length > 0) {
  441. const formData = new FormData();
  442. formData.append('file', fileInput.files[0]);
  443. formData.append('kodi_client_index', activeTvIndex);
  444. response = await fetch('/api/kodi/play_image', {
  445. method: 'POST',
  446. body: formData
  447. });
  448. } else {
  449. response = await fetch('/api/kodi/play_image', {
  450. method: 'POST',
  451. headers: { 'Content-Type': 'application/json' },
  452. body: JSON.stringify({
  453. image_url: urlInput,
  454. kodi_client_index: activeTvIndex
  455. })
  456. });
  457. }
  458. const result = await response.json();
  459. if (result.success) {
  460. showMessage(result.message);
  461. } else {
  462. showMessage(result.message, 'error');
  463. }
  464. } catch (error) {
  465. showMessage('网络错误: ' + error.message, 'error');
  466. } finally {
  467. showLoading(false);
  468. }
  469. }
  470. async function playRtspCurrentTv() {
  471. if (activeTvIndex === null) return;
  472. const rtspUrl = document.getElementById('modalRtspUrl').value.trim();
  473. const volume = parseInt(document.getElementById('modalRtspVolume').value);
  474. if (!rtspUrl) {
  475. showMessage('请输入RTSP视频流URL', 'error');
  476. return;
  477. }
  478. if (isNaN(volume) || volume < 0 || volume > 100) {
  479. showMessage('音量必须是0-100之间的整数', 'error');
  480. return;
  481. }
  482. try {
  483. showLoading(true);
  484. const response = await fetch('/api/kodi/play_rtsp', {
  485. method: 'POST',
  486. headers: { 'Content-Type': 'application/json' },
  487. body: JSON.stringify({
  488. rtsp_url: rtspUrl,
  489. kodi_client_index: activeTvIndex,
  490. volume: volume
  491. })
  492. });
  493. const result = await response.json();
  494. if (result.success) {
  495. showMessage(result.message);
  496. } else {
  497. showMessage(result.message, 'error');
  498. }
  499. } catch (error) {
  500. showMessage('网络错误: ' + error.message, 'error');
  501. } finally {
  502. showLoading(false);
  503. }
  504. }
  505. async function turnOnAllTvs() {
  506. if (!confirm('确定要唤醒所有电视吗?')) return;
  507. try {
  508. showLoading(true);
  509. const response = await fetch('/api/mitv/turn_on_all', {
  510. method: 'POST',
  511. headers: { 'Content-Type': 'application/json' }
  512. });
  513. const result = await response.json();
  514. if (result.success) {
  515. showMessage(result.message);
  516. } else {
  517. showMessage(result.message, 'error');
  518. }
  519. } catch (error) {
  520. showMessage('网络错误: ' + error.message, 'error');
  521. } finally {
  522. showLoading(false);
  523. }
  524. }
  525. async function turnOffAllTvs() {
  526. if (!confirm('确定要息屏所有电视吗?')) return;
  527. try {
  528. showLoading(true);
  529. const response = await fetch('/api/mitv/turn_off_all', {
  530. method: 'POST',
  531. headers: { 'Content-Type': 'application/json' }
  532. });
  533. const result = await response.json();
  534. if (result.success) {
  535. showMessage(result.message);
  536. } else {
  537. showMessage(result.message, 'error');
  538. }
  539. } catch (error) {
  540. showMessage('网络错误: ' + error.message, 'error');
  541. } finally {
  542. showLoading(false);
  543. }
  544. }
  545. async function getFreeTimePlayStatus() {
  546. const statusSpan = document.getElementById('freeTimeStatusText');
  547. const btnEnable = document.getElementById('btnEnableFreeTime');
  548. const btnDisable = document.getElementById('btnDisableFreeTime');
  549. if (!statusSpan) return;
  550. try {
  551. const response = await fetch('/api/kodi/free_time/status');
  552. const result = await response.json();
  553. if (result.success) {
  554. const isEnabled = result.data.enabled;
  555. if (isEnabled) {
  556. statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
  557. btnEnable.style.display = 'none';
  558. btnDisable.style.display = 'inline-block';
  559. } else {
  560. statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
  561. btnEnable.style.display = 'inline-block';
  562. btnDisable.style.display = 'none';
  563. }
  564. } else {
  565. statusSpan.textContent = '获取失败';
  566. }
  567. } catch (error) {
  568. console.error(error);
  569. statusSpan.textContent = '网络错误';
  570. }
  571. }
  572. async function controlFreeTimePlay(action) {
  573. try {
  574. showLoading(true);
  575. const response = await fetch('/api/kodi/free_time/control', {
  576. method: 'POST',
  577. headers: { 'Content-Type': 'application/json' },
  578. body: JSON.stringify({ action: action })
  579. });
  580. const result = await response.json();
  581. if (result.success) {
  582. showMessage(result.message);
  583. // 更新状态
  584. if (result.data) {
  585. const isEnabled = result.data.enabled;
  586. const statusSpan = document.getElementById('freeTimeStatusText');
  587. const btnEnable = document.getElementById('btnEnableFreeTime');
  588. const btnDisable = document.getElementById('btnDisableFreeTime');
  589. if (isEnabled) {
  590. statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
  591. btnEnable.style.display = 'none';
  592. btnDisable.style.display = 'inline-block';
  593. } else {
  594. statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
  595. btnEnable.style.display = 'inline-block';
  596. btnDisable.style.display = 'none';
  597. }
  598. } else {
  599. getFreeTimePlayStatus();
  600. }
  601. } else {
  602. showMessage(result.message, 'error');
  603. }
  604. } catch (error) {
  605. showMessage('网络错误: ' + error.message, 'error');
  606. } finally {
  607. showLoading(false);
  608. }
  609. }
  610. </script>
  611. {% endblock %}