base.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  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>展厅控制 - {% block title %}{% endblock %}</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. --sidebar-width: 260px;
  17. }
  18. * {
  19. margin: 0;
  20. padding: 0;
  21. box-sizing: border-box;
  22. }
  23. body {
  24. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  25. background: #f0f2f5;
  26. min-height: 100vh;
  27. color: var(--dark-text);
  28. display: flex;
  29. }
  30. /* 侧边栏样式 */
  31. .sidebar {
  32. width: var(--sidebar-width);
  33. background: #2c3e50;
  34. color: white;
  35. height: 100vh;
  36. position: fixed;
  37. left: 0;
  38. top: 0;
  39. display: flex;
  40. flex-direction: column;
  41. box-shadow: 2px 0 10px rgba(0,0,0,0.1);
  42. z-index: 1000;
  43. }
  44. .sidebar-header {
  45. padding: 30px 20px;
  46. text-align: center;
  47. border-bottom: 1px solid rgba(255,255,255,0.1);
  48. }
  49. .sidebar-header h1 {
  50. font-size: 1.5em;
  51. margin-bottom: 5px;
  52. color: white;
  53. }
  54. .sidebar-header p {
  55. font-size: 0.8em;
  56. color: #a0aeb9;
  57. }
  58. .nav-menu {
  59. flex: 1;
  60. padding: 20px 0;
  61. overflow-y: auto;
  62. }
  63. .nav-item {
  64. padding: 15px 25px;
  65. cursor: pointer;
  66. transition: all 0.3s;
  67. display: flex;
  68. align-items: center;
  69. gap: 12px;
  70. color: #bdc3c7;
  71. text-decoration: none;
  72. border-left: 4px solid transparent;
  73. }
  74. .nav-item:hover {
  75. background: rgba(255,255,255,0.05);
  76. color: white;
  77. }
  78. .nav-item.active {
  79. background: #34495e;
  80. color: var(--primary-color);
  81. border-left-color: var(--primary-color);
  82. }
  83. .sidebar-footer {
  84. padding: 20px;
  85. border-top: 1px solid rgba(255,255,255,0.1);
  86. }
  87. .logout-btn {
  88. display: block;
  89. text-align: center;
  90. padding: 10px;
  91. color: #ff6b6b;
  92. border: 1px solid #ff6b6b;
  93. border-radius: 6px;
  94. text-decoration: none;
  95. transition: all 0.3s;
  96. }
  97. .logout-btn:hover {
  98. background: #ff6b6b;
  99. color: white;
  100. }
  101. /* 主内容区样式 */
  102. .main-content {
  103. margin-left: var(--sidebar-width);
  104. flex: 1;
  105. padding: 30px;
  106. min-height: 100vh;
  107. background: var(--bg-gradient);
  108. }
  109. .module-header {
  110. margin-bottom: 25px;
  111. padding-bottom: 15px;
  112. border-bottom: 2px solid rgba(0,0,0,0.05);
  113. display: flex;
  114. justify-content: space-between;
  115. align-items: center;
  116. }
  117. .module-header h2 {
  118. font-size: 1.8em;
  119. color: #2c3e50;
  120. }
  121. .sub-section {
  122. background: var(--card-bg);
  123. border-radius: 12px;
  124. padding: 25px;
  125. margin-bottom: 25px;
  126. box-shadow: 0 4px 6px rgba(0,0,0,0.05);
  127. }
  128. .sub-section h3, .sub-section h4 {
  129. margin-bottom: 20px;
  130. color: #2c3e50;
  131. }
  132. .control-row {
  133. display: flex;
  134. flex-wrap: wrap;
  135. gap: 20px;
  136. align-items: flex-end;
  137. margin-bottom: 20px;
  138. }
  139. .control-group {
  140. flex: 1;
  141. min-width: 200px;
  142. }
  143. label {
  144. display: block;
  145. margin-bottom: 8px;
  146. font-weight: 600;
  147. color: #555;
  148. font-size: 0.9em;
  149. }
  150. input[type="text"],
  151. input[type="number"],
  152. select {
  153. width: 100%;
  154. padding: 12px;
  155. border: 1px solid #ddd;
  156. border-radius: 8px;
  157. font-size: 14px;
  158. transition: border-color 0.3s;
  159. }
  160. input[type="text"]:focus,
  161. input[type="number"]:focus,
  162. select:focus {
  163. border-color: var(--primary-color);
  164. outline: none;
  165. }
  166. input[type="file"] {
  167. padding: 8px 0;
  168. }
  169. .btn {
  170. padding: 12px 25px;
  171. border: none;
  172. border-radius: 8px;
  173. cursor: pointer;
  174. font-size: 14px;
  175. font-weight: 600;
  176. transition: all 0.2s;
  177. color: white;
  178. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  179. text-transform: uppercase;
  180. letter-spacing: 0.5px;
  181. }
  182. .btn:active {
  183. transform: translateY(1px);
  184. box-shadow: none;
  185. }
  186. .btn-primary { background-color: var(--primary-color); }
  187. .btn-primary:hover { background-color: #3dbdb4; }
  188. .btn-secondary { background-color: var(--secondary-color); }
  189. .btn-secondary:hover { background-color: #ff5252; }
  190. .btn-warning { background-color: var(--accent-color); color: #333; }
  191. .btn-warning:hover { background-color: #fdbf38; }
  192. .btn-info { background-color: #667eea; }
  193. .btn-info:hover { background-color: #5a6fd6; }
  194. /* Status & Message */
  195. .status-display {
  196. background: #e8f5e8;
  197. border: 1px solid #c8e6c9;
  198. color: #2e7d32;
  199. padding: 15px;
  200. border-radius: 8px;
  201. margin-top: 15px;
  202. font-size: 0.9em;
  203. }
  204. .status-display p { margin: 5px 0; }
  205. .message-box {
  206. position: fixed;
  207. top: 20px;
  208. right: 20px;
  209. padding: 15px 25px;
  210. border-radius: 8px;
  211. color: white;
  212. display: none;
  213. z-index: 3000;
  214. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  215. animation: slideIn 0.3s ease-out;
  216. }
  217. @keyframes slideIn {
  218. from { transform: translateX(100%); opacity: 0; }
  219. to { transform: translateX(0); opacity: 1; }
  220. }
  221. @keyframes slideInMobile {
  222. from { transform: translateY(120%); opacity: 0; }
  223. to { transform: translateY(0); opacity: 1; }
  224. }
  225. .msg-success { background-color: #4caf50; }
  226. .msg-error { background-color: #f44336; }
  227. .loading-overlay {
  228. position: fixed;
  229. top: 0; left: 0; right: 0; bottom: 0;
  230. background: rgba(255,255,255,0.7);
  231. display: none;
  232. justify-content: center;
  233. align-items: center;
  234. z-index: 4000;
  235. font-size: 1.2em;
  236. color: #555;
  237. flex-direction: column;
  238. }
  239. /* —— 移动端顶栏与抽屉导航(默认隐藏,见媒体查询)—— */
  240. .mobile-topbar {
  241. display: none;
  242. align-items: center;
  243. gap: 12px;
  244. padding: 10px 14px;
  245. padding-left: max(14px, env(safe-area-inset-left, 0px));
  246. padding-right: max(14px, env(safe-area-inset-right, 0px));
  247. padding-top: max(10px, env(safe-area-inset-top, 0px));
  248. background: #2c3e50;
  249. color: #fff;
  250. position: fixed;
  251. top: 0;
  252. left: 0;
  253. right: 0;
  254. z-index: 998;
  255. min-height: 48px;
  256. box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  257. }
  258. .mobile-menu-btn {
  259. display: flex;
  260. align-items: center;
  261. justify-content: center;
  262. width: 44px;
  263. height: 44px;
  264. margin: -6px 0 -6px -8px;
  265. padding: 0;
  266. border: none;
  267. border-radius: 8px;
  268. background: rgba(255,255,255,0.12);
  269. color: #fff;
  270. font-size: 1.35rem;
  271. line-height: 1;
  272. cursor: pointer;
  273. -webkit-tap-highlight-color: transparent;
  274. }
  275. .mobile-menu-btn:active {
  276. background: rgba(255,255,255,0.22);
  277. }
  278. .mobile-topbar-title {
  279. font-weight: 600;
  280. font-size: 1.05rem;
  281. flex: 1;
  282. min-width: 0;
  283. white-space: nowrap;
  284. overflow: hidden;
  285. text-overflow: ellipsis;
  286. }
  287. .sidebar-backdrop {
  288. display: none;
  289. position: fixed;
  290. inset: 0;
  291. background: rgba(0,0,0,0.45);
  292. z-index: 999;
  293. -webkit-tap-highlight-color: transparent;
  294. }
  295. .sidebar-backdrop.is-visible {
  296. display: block;
  297. }
  298. body.nav-open {
  299. overflow: hidden;
  300. touch-action: none;
  301. }
  302. @media (max-width: 768px) {
  303. .mobile-topbar {
  304. display: flex;
  305. }
  306. .sidebar {
  307. transform: translateX(-100%);
  308. transition: transform 0.25s ease;
  309. box-shadow: none;
  310. }
  311. .sidebar.sidebar-open {
  312. transform: translateX(0);
  313. box-shadow: 4px 0 24px rgba(0,0,0,0.25);
  314. }
  315. .main-content {
  316. margin-left: 0;
  317. width: 100%;
  318. padding: 16px;
  319. padding-bottom: max(16px, env(safe-area-inset-bottom, 0px));
  320. padding-top: calc(56px + env(safe-area-inset-top, 0px));
  321. }
  322. .module-header {
  323. flex-wrap: wrap;
  324. gap: 8px;
  325. }
  326. .module-header h2 {
  327. font-size: 1.35rem;
  328. }
  329. .sub-section {
  330. padding: 16px;
  331. }
  332. .control-row {
  333. flex-direction: column;
  334. align-items: stretch;
  335. }
  336. .control-group {
  337. min-width: 100%;
  338. flex: 1 1 auto !important;
  339. }
  340. input[type="text"],
  341. input[type="number"],
  342. input[type="email"],
  343. input[type="password"],
  344. select,
  345. textarea {
  346. font-size: 16px;
  347. }
  348. .btn {
  349. min-height: 44px;
  350. padding: 12px 20px;
  351. }
  352. .nav-item {
  353. min-height: 48px;
  354. padding: 14px 22px;
  355. }
  356. .logout-btn {
  357. min-height: 44px;
  358. padding: 12px;
  359. display: flex;
  360. align-items: center;
  361. justify-content: center;
  362. }
  363. .message-box {
  364. left: 12px;
  365. right: 12px;
  366. top: auto;
  367. bottom: max(16px, env(safe-area-inset-bottom, 0px));
  368. max-width: none;
  369. padding: 14px 18px;
  370. text-align: center;
  371. animation-name: slideInMobile;
  372. }
  373. }
  374. /* 模块特定样式 */
  375. {% block extra_styles %}{% endblock %}
  376. </style>
  377. </head>
  378. <body>
  379. <header class="mobile-topbar" id="mobileTopbar" aria-hidden="false">
  380. <button type="button" class="mobile-menu-btn" id="mobileMenuBtn" aria-label="打开导航菜单" aria-expanded="false">☰</button>
  381. <span class="mobile-topbar-title">展厅控制</span>
  382. </header>
  383. <div class="sidebar-backdrop" id="sidebarBackdrop" aria-hidden="true"></div>
  384. <!-- 侧边栏 -->
  385. <div class="sidebar" id="sidebar">
  386. <div class="sidebar-header">
  387. <h1>展厅控制</h1>
  388. <p>Admin Dashboard</p>
  389. </div>
  390. <nav class="nav-menu">
  391. <a href="{{ url_for('main.kodi_page') }}" class="nav-item {% if active_page == 'kodi' %}active{% endif %}">
  392. <span>📺</span> 电视控制 (Kodi)
  393. </a>
  394. <a href="{{ url_for('main.door_page') }}" class="nav-item {% if active_page == 'door' %}active{% endif %}">
  395. <span>🚪</span> 办公楼大门
  396. </a>
  397. <a href="{{ url_for('main.led_page') }}" class="nav-item {% if active_page == 'led' %}active{% endif %}">
  398. <span>💡</span> 展品灯座
  399. </a>
  400. <a href="{{ url_for('main.ha_page') }}" class="nav-item {% if active_page == 'ha' %}active{% endif %}">
  401. <span>💡</span> 展厅灯光
  402. </a>
  403. <a href="{{ url_for('main.self_check_page') }}" class="nav-item {% if active_page == 'self_check' %}active{% endif %}">
  404. <span>🛡️</span> 设备自检
  405. </a>
  406. <a href="{{ url_for('main.attachment_test_page') }}" class="nav-item {% if active_page == 'attachment_test' %}active{% endif %}">
  407. <span>📎</span> 附件上传测试
  408. </a>
  409. </nav>
  410. <div class="sidebar-footer">
  411. <a href="/logout" class="logout-btn">退出登录</a>
  412. </div>
  413. </div>
  414. <!-- 主内容区 -->
  415. <div class="main-content">
  416. {% block content %}
  417. {% endblock %}
  418. </div>
  419. <!-- 消息提示与遮罩 -->
  420. <div id="messageBox" class="message-box"></div>
  421. <div id="loadingOverlay" class="loading-overlay">
  422. <div><span style="font-size: 2em;">⏳</span><br>处理中...</div>
  423. </div>
  424. <script>
  425. // 通用函数
  426. function showMessage(message, type = 'success') {
  427. const msgBox = document.getElementById('messageBox');
  428. msgBox.textContent = message;
  429. msgBox.className = 'message-box ' + (type === 'success' ? 'msg-success' : 'msg-error');
  430. msgBox.style.display = 'block';
  431. // 重新触发动画
  432. msgBox.style.animation = 'none';
  433. msgBox.offsetHeight; /* trigger reflow */
  434. msgBox.style.animation = null;
  435. setTimeout(() => {
  436. msgBox.style.display = 'none';
  437. }, 3000);
  438. }
  439. function showLoading(show = true) {
  440. document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
  441. }
  442. (function () {
  443. const mq = window.matchMedia('(max-width: 768px)');
  444. const sidebar = document.getElementById('sidebar');
  445. const backdrop = document.getElementById('sidebarBackdrop');
  446. const menuBtn = document.getElementById('mobileMenuBtn');
  447. function closeMobileNav() {
  448. sidebar.classList.remove('sidebar-open');
  449. backdrop.classList.remove('is-visible');
  450. document.body.classList.remove('nav-open');
  451. backdrop.setAttribute('aria-hidden', 'true');
  452. if (menuBtn) {
  453. menuBtn.setAttribute('aria-expanded', 'false');
  454. }
  455. }
  456. function openMobileNav() {
  457. sidebar.classList.add('sidebar-open');
  458. backdrop.classList.add('is-visible');
  459. document.body.classList.add('nav-open');
  460. backdrop.setAttribute('aria-hidden', 'false');
  461. if (menuBtn) {
  462. menuBtn.setAttribute('aria-expanded', 'true');
  463. }
  464. }
  465. function toggleMobileNav() {
  466. if (sidebar.classList.contains('sidebar-open')) {
  467. closeMobileNav();
  468. } else {
  469. openMobileNav();
  470. }
  471. }
  472. if (menuBtn) {
  473. menuBtn.addEventListener('click', toggleMobileNav);
  474. }
  475. if (backdrop) {
  476. backdrop.addEventListener('click', closeMobileNav);
  477. }
  478. document.querySelectorAll('#sidebar .nav-item, #sidebar .logout-btn').forEach(function (el) {
  479. el.addEventListener('click', function () {
  480. if (mq.matches) {
  481. closeMobileNav();
  482. }
  483. });
  484. });
  485. window.addEventListener('resize', function () {
  486. if (!mq.matches) {
  487. closeMobileNav();
  488. }
  489. });
  490. document.addEventListener('keydown', function (e) {
  491. if (e.key === 'Escape' && mq.matches) {
  492. closeMobileNav();
  493. }
  494. });
  495. })();
  496. </script>
  497. {% block scripts %}
  498. {% endblock %}
  499. </body>
  500. </html>