index.html 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  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" style="width: 100%;">
  108. <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
  109. <label for="globalVolume">全局音量</label>
  110. <span id="volumeValue" style="font-weight: bold; color: #4ecdc4;">65</span>
  111. </div>
  112. <input type="range" id="globalVolume" min="0" max="100" value="65" style="width: 100%; cursor: pointer;" onchange="setGlobalVolume()" oninput="updateVolumeDisplay(this.value)">
  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. getGlobalVolume();
  207. window.onclick = function(event) {
  208. const modal = document.getElementById('tvConfigModal');
  209. if (event.target == modal) {
  210. closeTvModal();
  211. }
  212. }
  213. });
  214. async function getKodiStatus() {
  215. try {
  216. const response = await fetch('/api/kodi/status');
  217. const result = await response.json();
  218. if (result.success) {
  219. currentKodiStatus = result.data;
  220. document.getElementById('kodiStatusContent').innerHTML = `
  221. <p><strong>线程状态:</strong> ${currentKodiStatus.is_running ? '运行中' : '已停止'}</p>
  222. <p><strong>信息:</strong> ${currentKodiStatus.message}</p>
  223. `;
  224. } else {
  225. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">获取失败: ${result.message}</span>`;
  226. }
  227. } catch (error) {
  228. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">网络错误</span>`;
  229. }
  230. }
  231. async function getKodiClients() {
  232. const container = document.getElementById('tv-grid-container');
  233. if (!container) return;
  234. try {
  235. const response = await fetch('/api/kodi/clients');
  236. const result = await response.json();
  237. if (result.success && result.data.length > 0) {
  238. container.innerHTML = '';
  239. result.data.forEach(client => {
  240. const index = client.index;
  241. const name = client.name;
  242. const cardDiv = document.createElement('div');
  243. cardDiv.className = 'tv-card';
  244. cardDiv.onclick = () => openTvModal(index, name);
  245. cardDiv.innerHTML = `
  246. <div class="tv-icon">📺</div>
  247. <div class="tv-name">${name}</div>
  248. <div class="tv-index">ID: ${index}</div>
  249. <button class="btn btn-primary" style="padding: 5px 15px; font-size: 12px; margin-top: 5px;">
  250. ⚙️ 配置
  251. </button>
  252. `;
  253. container.appendChild(cardDiv);
  254. });
  255. } else {
  256. container.innerHTML = '<div style="width: 100%; text-align: center; color: #666;">未找到 Kodi 客户端</div>';
  257. }
  258. } catch (error) {
  259. console.error(error);
  260. container.innerHTML = '<div style="width: 100%; text-align: center; color: red;">加载失败</div>';
  261. }
  262. }
  263. async function getVideoList() {
  264. const selectEl = document.getElementById('videoSelect');
  265. try {
  266. const response = await fetch('/api/kodi/videos');
  267. const result = await response.json();
  268. if (result.success && result.data.length > 0) {
  269. selectEl.innerHTML = '';
  270. result.data.forEach(video => {
  271. const option = document.createElement('option');
  272. option.value = video.id;
  273. option.textContent = `${video.name} (ID: ${video.id})`;
  274. selectEl.appendChild(option);
  275. });
  276. } else {
  277. selectEl.innerHTML = '<option value="">未找到视频</option>';
  278. }
  279. } catch (error) {
  280. console.error(error);
  281. selectEl.innerHTML = '<option value="">加载失败</option>';
  282. }
  283. }
  284. async function startKodi() {
  285. const videoId = parseInt(document.getElementById('videoSelect').value);
  286. if (isNaN(videoId) || videoId < 0) {
  287. showMessage('请选择有效的视频', 'error');
  288. return;
  289. }
  290. try {
  291. showLoading(true);
  292. const response = await fetch('/api/kodi/start', {
  293. method: 'POST',
  294. headers: { 'Content-Type': 'application/json' },
  295. body: JSON.stringify({ video_id: videoId })
  296. });
  297. const result = await response.json();
  298. if (result.success) {
  299. showMessage(result.message);
  300. getKodiStatus();
  301. } else {
  302. showMessage(result.message, 'error');
  303. }
  304. } catch (error) {
  305. showMessage('网络错误: ' + error.message, 'error');
  306. } finally {
  307. showLoading(false);
  308. }
  309. }
  310. function updateVolumeDisplay(val) {
  311. document.getElementById('volumeValue').textContent = val;
  312. }
  313. async function getGlobalVolume() {
  314. try {
  315. const response = await fetch('/api/kodi/get_volume');
  316. const result = await response.json();
  317. if (result.success) {
  318. const vol = result.data.volume;
  319. document.getElementById('globalVolume').value = vol;
  320. updateVolumeDisplay(vol);
  321. }
  322. } catch (error) {
  323. console.error('获取音量失败', error);
  324. }
  325. }
  326. async function setGlobalVolume() {
  327. const volume = parseInt(document.getElementById('globalVolume').value);
  328. if (isNaN(volume) || volume < 0 || volume > 100) {
  329. showMessage('音量必须是 0-100 之间的整数', 'error');
  330. return;
  331. }
  332. try {
  333. // 滑动条体验优化:不显示全屏loading
  334. const response = await fetch('/api/kodi/set_volume', {
  335. method: 'POST',
  336. headers: { 'Content-Type': 'application/json' },
  337. body: JSON.stringify({ volume: volume })
  338. });
  339. const result = await response.json();
  340. if (result.success) {
  341. showMessage(result.message);
  342. } else {
  343. showMessage(result.message, 'error');
  344. }
  345. } catch (error) {
  346. showMessage('网络错误: ' + error.message, 'error');
  347. }
  348. }
  349. async function revokeIndividualState() {
  350. try {
  351. showLoading(true);
  352. const response = await fetch('/api/kodi/revoke_individual_state', {
  353. method: 'POST',
  354. headers: { 'Content-Type': 'application/json' }
  355. });
  356. const result = await response.json();
  357. if (result.success) {
  358. showMessage(result.message);
  359. getKodiStatus();
  360. } else {
  361. showMessage(result.message, 'error');
  362. }
  363. } catch (error) {
  364. showMessage('网络错误: ' + error.message, 'error');
  365. } finally {
  366. showLoading(false);
  367. }
  368. }
  369. async function startAllKodiApps() {
  370. try {
  371. showLoading(true);
  372. const response = await fetch('/api/kodi/start_all_apps', {
  373. method: 'POST',
  374. headers: { 'Content-Type': 'application/json' }
  375. });
  376. const result = await response.json();
  377. if (result.success) {
  378. showMessage(result.message);
  379. getKodiStatus();
  380. } else {
  381. showMessage(result.message, 'error');
  382. }
  383. } catch (error) {
  384. showMessage('网络错误: ' + error.message, 'error');
  385. } finally {
  386. showLoading(false);
  387. }
  388. }
  389. function openTvModal(index, name) {
  390. activeTvIndex = index;
  391. document.getElementById('modalTvName').textContent = `${name} (Index: ${index})`;
  392. document.getElementById('modalImageFile').value = '';
  393. document.getElementById('modalImageUrl').value = '';
  394. document.getElementById('modalRtspUrl').value = '';
  395. document.getElementById('modalRtspVolume').value = '0';
  396. document.getElementById('tvConfigModal').style.display = 'flex';
  397. }
  398. function closeTvModal() {
  399. document.getElementById('tvConfigModal').style.display = 'none';
  400. activeTvIndex = null;
  401. }
  402. async function turnOnCurrentTv() {
  403. if (activeTvIndex === null) return;
  404. try {
  405. showLoading(true);
  406. const response = await fetch('/api/mitv/turn_on', {
  407. method: 'POST',
  408. headers: { 'Content-Type': 'application/json' },
  409. body: JSON.stringify({ kodi_id: activeTvIndex })
  410. });
  411. const result = await response.json();
  412. if (result.success) {
  413. showMessage(result.message);
  414. } else {
  415. showMessage(result.message, 'error');
  416. }
  417. } catch (error) {
  418. showMessage('网络错误: ' + error.message, 'error');
  419. } finally {
  420. showLoading(false);
  421. }
  422. }
  423. async function turnOffCurrentTv() {
  424. if (activeTvIndex === null) return;
  425. try {
  426. showLoading(true);
  427. const response = await fetch('/api/mitv/turn_off', {
  428. method: 'POST',
  429. headers: { 'Content-Type': 'application/json' },
  430. body: JSON.stringify({ kodi_id: activeTvIndex })
  431. });
  432. const result = await response.json();
  433. if (result.success) {
  434. showMessage(result.message);
  435. } else {
  436. showMessage(result.message, 'error');
  437. }
  438. } catch (error) {
  439. showMessage('网络错误: ' + error.message, 'error');
  440. } finally {
  441. showLoading(false);
  442. }
  443. }
  444. async function playImageCurrentTv() {
  445. if (activeTvIndex === null) return;
  446. const fileInput = document.getElementById('modalImageFile');
  447. const urlInput = document.getElementById('modalImageUrl').value.trim();
  448. if ((!fileInput.files || fileInput.files.length === 0) && !urlInput) {
  449. showMessage('请选择图片文件或输入图片URL', 'error');
  450. return;
  451. }
  452. try {
  453. showLoading(true);
  454. let response;
  455. if (fileInput.files && fileInput.files.length > 0) {
  456. const formData = new FormData();
  457. formData.append('file', fileInput.files[0]);
  458. formData.append('kodi_client_index', activeTvIndex);
  459. response = await fetch('/api/kodi/play_image', {
  460. method: 'POST',
  461. body: formData
  462. });
  463. } else {
  464. response = await fetch('/api/kodi/play_image', {
  465. method: 'POST',
  466. headers: { 'Content-Type': 'application/json' },
  467. body: JSON.stringify({
  468. image_url: urlInput,
  469. kodi_client_index: activeTvIndex
  470. })
  471. });
  472. }
  473. const result = await response.json();
  474. if (result.success) {
  475. showMessage(result.message);
  476. } else {
  477. showMessage(result.message, 'error');
  478. }
  479. } catch (error) {
  480. showMessage('网络错误: ' + error.message, 'error');
  481. } finally {
  482. showLoading(false);
  483. }
  484. }
  485. async function playRtspCurrentTv() {
  486. if (activeTvIndex === null) return;
  487. const rtspUrl = document.getElementById('modalRtspUrl').value.trim();
  488. const volume = parseInt(document.getElementById('modalRtspVolume').value);
  489. if (!rtspUrl) {
  490. showMessage('请输入RTSP视频流URL', 'error');
  491. return;
  492. }
  493. if (isNaN(volume) || volume < 0 || volume > 100) {
  494. showMessage('音量必须是0-100之间的整数', 'error');
  495. return;
  496. }
  497. try {
  498. showLoading(true);
  499. const response = await fetch('/api/kodi/play_rtsp', {
  500. method: 'POST',
  501. headers: { 'Content-Type': 'application/json' },
  502. body: JSON.stringify({
  503. rtsp_url: rtspUrl,
  504. kodi_client_index: activeTvIndex,
  505. volume: volume
  506. })
  507. });
  508. const result = await response.json();
  509. if (result.success) {
  510. showMessage(result.message);
  511. } else {
  512. showMessage(result.message, 'error');
  513. }
  514. } catch (error) {
  515. showMessage('网络错误: ' + error.message, 'error');
  516. } finally {
  517. showLoading(false);
  518. }
  519. }
  520. async function turnOnAllTvs() {
  521. if (!confirm('确定要唤醒所有电视吗?')) return;
  522. try {
  523. showLoading(true);
  524. const response = await fetch('/api/mitv/turn_on_all', {
  525. method: 'POST',
  526. headers: { 'Content-Type': 'application/json' }
  527. });
  528. const result = await response.json();
  529. if (result.success) {
  530. showMessage(result.message);
  531. } else {
  532. showMessage(result.message, 'error');
  533. }
  534. } catch (error) {
  535. showMessage('网络错误: ' + error.message, 'error');
  536. } finally {
  537. showLoading(false);
  538. }
  539. }
  540. async function turnOffAllTvs() {
  541. if (!confirm('确定要息屏所有电视吗?')) return;
  542. try {
  543. showLoading(true);
  544. const response = await fetch('/api/mitv/turn_off_all', {
  545. method: 'POST',
  546. headers: { 'Content-Type': 'application/json' }
  547. });
  548. const result = await response.json();
  549. if (result.success) {
  550. showMessage(result.message);
  551. } else {
  552. showMessage(result.message, 'error');
  553. }
  554. } catch (error) {
  555. showMessage('网络错误: ' + error.message, 'error');
  556. } finally {
  557. showLoading(false);
  558. }
  559. }
  560. async function getFreeTimePlayStatus() {
  561. const statusSpan = document.getElementById('freeTimeStatusText');
  562. const btnEnable = document.getElementById('btnEnableFreeTime');
  563. const btnDisable = document.getElementById('btnDisableFreeTime');
  564. if (!statusSpan) return;
  565. try {
  566. const response = await fetch('/api/kodi/free_time/status');
  567. const result = await response.json();
  568. if (result.success) {
  569. const isEnabled = result.data.enabled;
  570. if (isEnabled) {
  571. statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
  572. btnEnable.style.display = 'none';
  573. btnDisable.style.display = 'inline-block';
  574. } else {
  575. statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
  576. btnEnable.style.display = 'inline-block';
  577. btnDisable.style.display = 'none';
  578. }
  579. } else {
  580. statusSpan.textContent = '获取失败';
  581. }
  582. } catch (error) {
  583. console.error(error);
  584. statusSpan.textContent = '网络错误';
  585. }
  586. }
  587. async function controlFreeTimePlay(action) {
  588. try {
  589. showLoading(true);
  590. const response = await fetch('/api/kodi/free_time/control', {
  591. method: 'POST',
  592. headers: { 'Content-Type': 'application/json' },
  593. body: JSON.stringify({ action: action })
  594. });
  595. const result = await response.json();
  596. if (result.success) {
  597. showMessage(result.message);
  598. // 更新状态
  599. if (result.data) {
  600. const isEnabled = result.data.enabled;
  601. const statusSpan = document.getElementById('freeTimeStatusText');
  602. const btnEnable = document.getElementById('btnEnableFreeTime');
  603. const btnDisable = document.getElementById('btnDisableFreeTime');
  604. if (isEnabled) {
  605. statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
  606. btnEnable.style.display = 'none';
  607. btnDisable.style.display = 'inline-block';
  608. } else {
  609. statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
  610. btnEnable.style.display = 'inline-block';
  611. btnDisable.style.display = 'none';
  612. }
  613. } else {
  614. getFreeTimePlayStatus();
  615. }
  616. } else {
  617. showMessage(result.message, 'error');
  618. }
  619. } catch (error) {
  620. showMessage('网络错误: ' + error.message, 'error');
  621. } finally {
  622. showLoading(false);
  623. }
  624. }
  625. </script>
  626. {% endblock %}