base.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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. <script src="https://cdn.tailwindcss.com"></script>
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
  9. <script>
  10. tailwind.config = {
  11. theme: {
  12. extend: {
  13. colors: {
  14. primary: '#6366f1',
  15. secondary: '#8b5cf6',
  16. accent: '#f43f5e',
  17. }
  18. }
  19. }
  20. }
  21. </script>
  22. <style>
  23. body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; }
  24. .sidebar-link.active { background: rgba(99,102,241,0.10); color: #6366f1; border-right: 3px solid #6366f1; }
  25. .image-card:hover .image-actions { opacity: 1; }
  26. .image-actions { opacity: 0; transition: opacity 0.2s; }
  27. ::-webkit-scrollbar { width: 6px; height: 6px; }
  28. ::-webkit-scrollbar-track { background: #f1f5f9; }
  29. ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
  30. .modal-overlay { backdrop-filter: blur(6px); }
  31. /* Lightbox */
  32. #lightbox { cursor: zoom-out; }
  33. #lightbox img { max-height: 85vh; max-width: 90vw; object-fit: contain; }
  34. .date-chip { transition: all 0.15s; }
  35. .date-chip:hover { transform: translateY(-1px); }
  36. /* Sidebar collapse */
  37. .sidebar { transition: width 0.25s ease; }
  38. .sidebar.collapsed { width: 5rem; }
  39. .sidebar.collapsed .sidebar-text { display: none; }
  40. .sidebar.collapsed .sidebar-sub { display: none; }
  41. .sidebar.collapsed .brand-text { display: none; }
  42. .sidebar.collapsed .collapse-btn i { transform: rotate(180deg); }
  43. .sidebar.collapsed main { margin-left: 5rem !important; }
  44. .sidebar-sub { overflow: hidden; transition: max-height 0.25s ease, opacity 0.2s; max-height: 500px; opacity: 1; }
  45. .sidebar-sub.collapsed { max-height: 0 !important; opacity: 0 !important; }
  46. .subject-toggle { transition: transform 0.2s; display: inline-block; }
  47. .subject-toggle.open { transform: rotate(90deg); }
  48. .subject-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.75rem; border-radius: 0.5rem; font-size: 0.8rem; color: #6b7280; text-decoration: none; transition: all 0.15s; cursor: pointer; white-space: nowrap; }
  49. .subject-item:hover { background: #f3f4f6; color: #374151; }
  50. .subject-item.active { background: rgba(99,102,241,0.10); color: #6366f1; }
  51. .sidebar-section-label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.08em; color: #9ca3af; padding: 0.5rem 1rem 0.25rem; font-weight: 600; }
  52. .sidebar.collapsed .sidebar-section-label { display: none; }
  53. </style>
  54. {% block extra_css %}{% endblock %}
  55. </head>
  56. <body class="bg-gray-50 min-h-screen">
  57. <div class="flex min-h-screen">
  58. <!-- Sidebar -->
  59. <aside class="sidebar w-56 bg-white shadow-lg fixed h-full z-20 flex flex-col border-r border-gray-100">
  60. <!-- Header -->
  61. <div class="px-4 py-4 flex items-center gap-3">
  62. <div class="w-9 h-9 rounded-xl bg-primary flex items-center justify-center text-white text-base flex-shrink-0">
  63. <i class="fa-solid fa-book-open"></i>
  64. </div>
  65. <div class="brand-text overflow-hidden">
  66. <h1 class="font-bold text-gray-800 text-sm leading-tight">满意错题本</h1>
  67. <p class="text-[10px] text-gray-400 mt-0.5">在线复习助手</p>
  68. </div>
  69. <button id="collapseBtn" class="collapse-btn ml-auto w-7 h-7 rounded-lg flex items-center justify-center text-gray-400 hover:bg-gray-100 hover:text-gray-600 transition flex-shrink-0" title="收起/展开">
  70. <i class="fa-solid fa-angles-left text-xs"></i>
  71. </button>
  72. </div>
  73. <!-- Nav -->
  74. <nav class="flex-1 py-2 px-3 overflow-y-auto">
  75. <!-- 首页 -->
  76. <a href="/" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition mb-1 {% if request.url.path == '/' %}active{% endif %}">
  77. <i class="fa-solid fa-house w-5 text-center text-base flex-shrink-0"></i>
  78. <span class="sidebar-text text-sm font-medium">首页</span>
  79. </a>
  80. <!-- 全部科目 -->
  81. <div class="mb-1">
  82. <a href="javascript:void(0)" id="subjectToggleBtn" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition">
  83. <i class="fa-solid fa-list-check w-5 text-center text-base flex-shrink-0"></i>
  84. <span class="sidebar-text text-sm font-medium">全部科目</span>
  85. <i class="fa-solid fa-chevron-right text-[10px] text-gray-400 ml-auto subject-toggle" id="subjectToggleIcon"></i>
  86. </a>
  87. <div id="subjectSubmenu" class="sidebar-sub collapsed mt-1 ml-4 pl-3 border-l border-gray-100">
  88. <div class="sidebar-section-label sidebar-text px-1">科目列表</div>
  89. {% for s in subjects %}
  90. <a href="/subject/{{ s.id }}" class="subject-item {% if request.url.path == '/subject/' ~ s.id|string %}active{% endif %}">
  91. <span class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background-color:{{ s.color }};"></span>
  92. <span class="sidebar-text truncate">{{ s.name }}</span>
  93. <span class="sidebar-text ml-auto text-[10px] text-gray-300">{{ s.error_images|length }}</span>
  94. </a>
  95. {% endfor %}
  96. {% if not subjects %}
  97. <div class="sidebar-text text-xs text-gray-300 px-3 py-2">暂无科目</div>
  98. {% endif %}
  99. </div>
  100. </div>
  101. <!-- 科目管理 -->
  102. <a href="/subjects" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition mb-1 {% if '/subjects' in request.url.path %}active{% endif %}">
  103. <i class="fa-solid fa-folder w-5 text-center text-base flex-shrink-0"></i>
  104. <span class="sidebar-text text-sm font-medium">科目管理</span>
  105. </a>
  106. <!-- 知识评价 -->
  107. <a href="/evaluations" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition mb-1 {% if '/evaluations' in request.url.path %}active{% endif %}">
  108. <i class="fa-solid fa-clipboard-check w-5 text-center text-base flex-shrink-0"></i>
  109. <span class="sidebar-text text-sm font-medium">知识评价</span>
  110. </a>
  111. <!-- 时间线 New -->
  112. <a href="/timeline-new" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition mb-1 {% if '/timeline-new' in request.url.path %}active{% endif %}">
  113. <i class="fa-solid fa-timeline w-5 text-center text-base flex-shrink-0"></i>
  114. <span class="sidebar-text text-sm font-medium">时间线 New</span>
  115. </a>
  116. <!-- 时间线 -->
  117. <a href="/timeline" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition mb-1 {% if '/timeline' in request.url.path %}active{% endif %}">
  118. <i class="fa-solid fa-clock-rotate-left w-5 text-center text-base flex-shrink-0"></i>
  119. <span class="sidebar-text text-sm font-medium">时间线</span>
  120. </a>
  121. <!-- 复习汇总 -->
  122. <a href="/review" class="sidebar-link flex items-center gap-3 px-3 py-2.5 rounded-xl text-gray-500 hover:bg-gray-50 transition mb-1 {% if '/review' in request.url.path %}active{% endif %}">
  123. <i class="fa-solid fa-graduation-cap w-5 text-center text-base flex-shrink-0"></i>
  124. <span class="sidebar-text text-sm font-medium">复习汇总</span>
  125. </a>
  126. </nav>
  127. <!-- Footer -->
  128. <div class="px-4 py-3 border-t border-gray-100">
  129. <p class="sidebar-text text-[10px] text-gray-300 text-center">© 2025 满意错题本</p>
  130. </div>
  131. </aside>
  132. <!-- Main content -->
  133. <main class="flex-1 ml-56 transition-margin">
  134. <header class="bg-white/80 backdrop-blur border-b border-gray-100 px-8 py-3.5 flex items-center justify-between sticky top-0 z-10">
  135. <h2 class="text-base font-semibold text-gray-700">{% block page_title %}{% endblock %}</h2>
  136. <div class="text-xs text-gray-400" id="current-date"></div>
  137. </header>
  138. <div class="p-8">
  139. {% block content %}{% endblock %}
  140. </div>
  141. </main>
  142. </div>
  143. <!-- Lightbox -->
  144. <div id="lightbox" class="fixed inset-0 z-50 hidden items-center justify-center modal-overlay bg-black/70" onclick="closeLightbox(event)">
  145. <button class="absolute top-4 right-4 text-white/80 hover:text-white text-2xl z-10" onclick="closeLightbox(event)">
  146. <i class="fa-solid fa-xmark"></i>
  147. </button>
  148. <button id="lb-prev" class="absolute left-4 text-white/80 hover:text-white text-2xl hidden" onclick="navigateLightbox(-1,event)">
  149. <i class="fa-solid fa-chevron-left"></i>
  150. </button>
  151. <button id="lb-next" class="absolute right-4 text-white/80 hover:text-white text-2xl hidden" onclick="navigateLightbox(1,event)">
  152. <i class="fa-solid fa-chevron-right"></i>
  153. </button>
  154. <img id="lb-img" src="" alt="" class="rounded-lg shadow-2xl max-h-[85vh] max-w-[90vw] object-contain">
  155. <p id="lb-caption" class="absolute bottom-4 left-1/2 -translate-x-1/2 text-white/80 text-sm bg-black/40 px-4 py-1.5 rounded-full"></p>
  156. </div>
  157. <script>
  158. document.getElementById('current-date').textContent = new Date().toLocaleDateString('zh-CN', {year:'numeric',month:'long',day:'numeric',weekday:'long'});
  159. // Sidebar collapse
  160. (function(){
  161. var sidebar = document.querySelector('.sidebar');
  162. var main = document.querySelector('main');
  163. var btn = document.getElementById('collapseBtn');
  164. var icon = btn ? btn.querySelector('i') : null;
  165. var collapsed = localStorage.getItem('sidebarCollapsed') === 'true';
  166. if(collapsed){
  167. sidebar.classList.add('collapsed');
  168. main.style.marginLeft = '5rem';
  169. if(icon) icon.className = 'fa-solid fa-angles-right text-xs';
  170. }
  171. if(btn){
  172. btn.addEventListener('click', function(){
  173. sidebar.classList.toggle('collapsed');
  174. if(sidebar.classList.contains('collapsed')){
  175. main.style.marginLeft = '5rem';
  176. if(icon) icon.className = 'fa-solid fa-angles-right text-xs';
  177. localStorage.setItem('sidebarCollapsed', 'true');
  178. } else {
  179. main.style.marginLeft = '14rem';
  180. if(icon) icon.className = 'fa-solid fa-angles-left text-xs';
  181. localStorage.setItem('sidebarCollapsed', 'false');
  182. }
  183. });
  184. }
  185. })();
  186. // Subject submenu toggle (collapsed by default)
  187. (function(){
  188. var submenu = document.getElementById('subjectSubmenu');
  189. var icon = document.getElementById('subjectToggleIcon');
  190. var btn = document.getElementById('subjectToggleBtn');
  191. var saved = localStorage.getItem('subjectMenuOpen');
  192. var isOpen = saved === 'true';
  193. if(isOpen){
  194. submenu.classList.remove('collapsed');
  195. if(icon) icon.classList.add('open');
  196. }
  197. if(btn){
  198. btn.addEventListener('click', function(e){
  199. e.preventDefault();
  200. e.stopPropagation();
  201. isOpen = !isOpen;
  202. if(isOpen){
  203. submenu.classList.remove('collapsed');
  204. if(icon) icon.classList.add('open');
  205. } else {
  206. submenu.classList.add('collapsed');
  207. if(icon) icon.classList.remove('open');
  208. }
  209. localStorage.setItem('subjectMenuOpen', String(isOpen));
  210. });
  211. }
  212. })();
  213. // Lightbox
  214. let lbImages = [];
  215. let lbIndex = 0;
  216. function openLightbox(src, caption, allImages, index) {
  217. lbImages = allImages || [src];
  218. lbIndex = index || 0;
  219. updateLightbox();
  220. const lb = document.getElementById('lightbox');
  221. lb.style.display='flex';
  222. document.body.style.overflow = 'hidden';
  223. }
  224. function closeLightbox(e) {
  225. if (e && e.target.tagName === 'IMG') return;
  226. const lb = document.getElementById('lightbox');
  227. lb.style.display='none';
  228. document.body.style.overflow = '';
  229. }
  230. function navigateLightbox(dir, e) {
  231. if (e) e.stopPropagation();
  232. if (lbImages.length <= 1) return;
  233. lbIndex = (lbIndex + dir + lbImages.length) % lbImages.length;
  234. updateLightbox();
  235. }
  236. function updateLightbox() {
  237. document.getElementById('lb-img').src = lbImages[lbIndex];
  238. document.getElementById('lb-caption').textContent = lbImages[lbIndex + 1] ? `${lbIndex+1}/${lbImages.length}` : '';
  239. document.getElementById('lb-prev').classList.toggle('hidden', lbImages.length <= 1);
  240. document.getElementById('lb-next').classList.toggle('hidden', lbImages.length <= 1);
  241. }
  242. document.addEventListener('keydown', e => {
  243. if (document.getElementById('lightbox').style.display === 'none' || document.getElementById('lightbox').style.display === '') return;
  244. if (e.key === 'ArrowLeft') navigateLightbox(-1);
  245. else if (e.key === 'ArrowRight') navigateLightbox(1);
  246. else if (e.key === 'Escape') closeLightbox(e);
  247. });
  248. </script>
  249. {% block extra_js %}{% endblock %}
  250. </body>
  251. </html>