mistpe commited on
Commit
50b254a
·
verified ·
1 Parent(s): 3862c05

Upload 4 files

Browse files
app/templates/article.html ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{{ article.title }} - 个人博客{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
7
+ <style>
8
+ /* 文章容器样式 */
9
+ .article-container {
10
+ max-width: 900px;
11
+ margin: 0 auto;
12
+ background: white;
13
+ border-radius: 20px;
14
+ box-shadow: 0 2px 12px rgba(99, 145, 197, 0.08);
15
+ border: 2px solid var(--light-blue);
16
+ padding: 2.5rem;
17
+ }
18
+
19
+ /* 文章头部样式 */
20
+ .article-header {
21
+ margin-bottom: 2.5rem;
22
+ padding-bottom: 1.5rem;
23
+ border-bottom: 1px solid var(--light-blue);
24
+ }
25
+
26
+ .article-title {
27
+ font-size: 2.5rem;
28
+ font-weight: 700;
29
+ color: var(--text-dark);
30
+ line-height: 1.3;
31
+ margin-bottom: 1rem;
32
+ background: linear-gradient(135deg, var(--primary-blue), var(--soft-purple));
33
+ -webkit-background-clip: text;
34
+ -webkit-text-fill-color: transparent;
35
+ }
36
+
37
+ .article-meta {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 1.5rem;
41
+ color: #64748B;
42
+ }
43
+
44
+ .meta-item {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 0.5rem;
48
+ }
49
+
50
+ .meta-item i {
51
+ color: var(--primary-blue);
52
+ }
53
+
54
+ /* AI 摘要样式 */
55
+ .article-summary {
56
+ background: var(--warm-cream);
57
+ border-radius: 16px;
58
+ padding: 1.5rem;
59
+ margin: 2rem 0;
60
+ position: relative;
61
+ }
62
+
63
+ .summary-label {
64
+ position: absolute;
65
+ top: -12px;
66
+ left: 16px;
67
+ background: var(--primary-blue);
68
+ color: white;
69
+ padding: 0.25rem 1rem;
70
+ border-radius: 20px;
71
+ font-size: 0.875rem;
72
+ font-weight: 500;
73
+ }
74
+
75
+ /* 文章内容样式 */
76
+ .article-content {
77
+ line-height: 1.8;
78
+ color: var(--text-dark);
79
+ }
80
+
81
+ .markdown-body {
82
+ font-size: 1.1rem;
83
+ }
84
+
85
+ .markdown-body h1,
86
+ .markdown-body h2,
87
+ .markdown-body h3 {
88
+ color: var(--primary-blue);
89
+ margin-top: 2em;
90
+ margin-bottom: 1em;
91
+ font-weight: 600;
92
+ }
93
+
94
+ .markdown-body p {
95
+ margin-bottom: 1.5em;
96
+ }
97
+
98
+ .markdown-body a {
99
+ color: var(--primary-blue);
100
+ text-decoration: none;
101
+ border-bottom: 1px dashed var(--light-blue);
102
+ transition: all 0.3s;
103
+ }
104
+
105
+ .markdown-body a:hover {
106
+ border-bottom-style: solid;
107
+ color: var(--soft-purple);
108
+ }
109
+
110
+ .markdown-body code {
111
+ background: #F8FAFC;
112
+ padding: 0.2em 0.4em;
113
+ border-radius: 4px;
114
+ font-size: 0.9em;
115
+ color: var(--primary-blue);
116
+ }
117
+
118
+ .markdown-body pre {
119
+ background: #F8FAFC;
120
+ border-radius: 12px;
121
+ padding: 1rem;
122
+ overflow-x: auto;
123
+ border: 1px solid var(--light-blue);
124
+ }
125
+
126
+ .markdown-body pre code {
127
+ background: none;
128
+ padding: 0;
129
+ color: inherit;
130
+ }
131
+
132
+ .markdown-body blockquote {
133
+ border-left: 4px solid var(--light-blue);
134
+ padding: 0.5rem 0 0.5rem 1rem;
135
+ margin: 1.5rem 0;
136
+ color: #64748B;
137
+ background: #F8FAFC;
138
+ }
139
+
140
+ .markdown-body img {
141
+ max-width: 100%;
142
+ border-radius: 12px;
143
+ margin: 1.5rem 0;
144
+ }
145
+
146
+ /* AI 聊天窗口样式 */
147
+ .chat-toggle {
148
+ position: fixed;
149
+ right: 2rem;
150
+ bottom: 2rem;
151
+ width: 56px;
152
+ height: 56px;
153
+ border-radius: 28px;
154
+ background: linear-gradient(135deg, var(--primary-blue), var(--light-blue));
155
+ color: white;
156
+ border: none;
157
+ cursor: pointer;
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ font-size: 1.5rem;
162
+ box-shadow: 0 4px 12px rgba(99, 145, 197, 0.2);
163
+ transition: all 0.3s;
164
+ z-index: 998;
165
+ }
166
+
167
+ .chat-toggle:hover {
168
+ transform: translateY(-2px);
169
+ box-shadow: 0 6px 16px rgba(99, 145, 197, 0.3);
170
+ }
171
+
172
+ .chat-window {
173
+ position: fixed;
174
+ right: 2rem;
175
+ bottom: 2rem;
176
+ width: 380px;
177
+ height: 600px;
178
+ background: white;
179
+ border-radius: 20px;
180
+ box-shadow: 0 4px 20px rgba(99, 145, 197, 0.15);
181
+ display: flex;
182
+ flex-direction: column;
183
+ transform: scale(0);
184
+ opacity: 0;
185
+ transform-origin: bottom right;
186
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
187
+ z-index: 999;
188
+ border: 1px solid var(--light-blue);
189
+ }
190
+
191
+ .chat-window.active {
192
+ transform: scale(1);
193
+ opacity: 1;
194
+ }
195
+
196
+ .chat-header {
197
+ padding: 1.25rem;
198
+ background: linear-gradient(135deg, var(--primary-blue), var(--light-blue));
199
+ color: white;
200
+ border-radius: 20px 20px 0 0;
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 0.75rem;
204
+ }
205
+
206
+ .chat-title {
207
+ font-weight: 600;
208
+ flex: 1;
209
+ }
210
+
211
+ .chat-close {
212
+ background: none;
213
+ border: none;
214
+ color: white;
215
+ cursor: pointer;
216
+ width: 32px;
217
+ height: 32px;
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ border-radius: 16px;
222
+ transition: all 0.3s;
223
+ }
224
+
225
+ .chat-close:hover {
226
+ background: rgba(255, 255, 255, 0.2);
227
+ }
228
+
229
+ .chat-messages {
230
+ flex: 1;
231
+ overflow-y: auto;
232
+ overflow-x: hidden;
233
+ padding: 1.5rem;
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: 1rem;
237
+ width: 100%;
238
+ }
239
+
240
+ .chat-message {
241
+ max-width: 85%;
242
+ padding: 1rem;
243
+ border-radius: 16px;
244
+ line-height: 1.3;
245
+ animation: messageSlide 0.3s ease;
246
+ word-wrap: break-word;
247
+ overflow-wrap: break-word;
248
+ width: fit-content;
249
+ }
250
+
251
+ .chat-message p {
252
+ margin: 0;
253
+ white-space: pre-wrap;
254
+ }
255
+
256
+ .chat-message img,
257
+ .chat-message pre,
258
+ .chat-message code {
259
+ max-width: 100%;
260
+ overflow-x: hidden;
261
+ }
262
+ .chat-message.user {
263
+ background: var(--primary-blue);
264
+ color: white;
265
+ margin-left: auto;
266
+ }
267
+
268
+ .chat-message.assistant {
269
+ background: #7b85b8;
270
+ color: white;
271
+ margin-right: auto;
272
+ }
273
+
274
+ .chat-input-container {
275
+ padding: 1.25rem;
276
+ border-top: 1px solid var(--light-blue);
277
+ }
278
+
279
+ .chat-input-wrapper {
280
+ display: flex;
281
+ gap: 0.75rem;
282
+ align-items: flex-end;
283
+ }
284
+
285
+ .chat-input {
286
+ flex: 1;
287
+ min-height: 44px;
288
+ max-height: 120px;
289
+ padding: 0.75rem 1rem;
290
+ border: 2px solid var(--light-blue);
291
+ border-radius: 12px;
292
+ resize: none;
293
+ font-size: 1rem;
294
+ line-height: 1.5;
295
+ transition: all 0.3s;
296
+ }
297
+
298
+ .chat-input:focus {
299
+ outline: none;
300
+ border-color: var(--primary-blue);
301
+ box-shadow: 0 0 0 3px rgba(99, 145, 197, 0.1);
302
+ }
303
+
304
+ .chat-send {
305
+ background: var(--primary-blue);
306
+ color: white;
307
+ width: 44px;
308
+ height: 44px;
309
+ border: none;
310
+ border-radius: 12px;
311
+ cursor: pointer;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ transition: all 0.3s;
316
+ }
317
+
318
+ .chat-send:hover {
319
+ background: var(--light-blue);
320
+ transform: translateY(-2px);
321
+ }
322
+
323
+ /* 动画 */
324
+ @keyframes messageSlide {
325
+ from {
326
+ opacity: 0;
327
+ transform: translateY(10px);
328
+ }
329
+ to {
330
+ opacity: 1;
331
+ transform: translateY(0);
332
+ }
333
+ }
334
+
335
+ /* 响应式设计 */
336
+ @media (max-width: 768px) {
337
+ .article-container {
338
+ padding: 1.5rem;
339
+ border-radius: 12px;
340
+ }
341
+
342
+ .article-title {
343
+ font-size: 2rem;
344
+ }
345
+
346
+ .chat-window {
347
+ right: 1rem;
348
+ bottom: 1rem;
349
+ left: 1rem;
350
+ width: auto;
351
+ height: 500px;
352
+ }
353
+
354
+ .chat-toggle {
355
+ right: 1rem;
356
+ bottom: 1rem;
357
+ }
358
+ }
359
+ </style>
360
+ {% endblock %}
361
+
362
+ {% block content %}
363
+ <!-- 文章内容 -->
364
+ <article class="article-container">
365
+ <header class="article-header">
366
+ <h1 class="article-title">{{ article.title }}</h1>
367
+ <div class="article-meta">
368
+ <div class="meta-item">
369
+ <i class="fas fa-calendar"></i>
370
+ <span>{{ article.created_at.strftime('%Y-%m-%d') }}</span>
371
+ </div>
372
+ </div>
373
+ </header>
374
+
375
+ {% if article.summary %}
376
+ <div class="article-summary">
377
+ <span class="summary-label">AI 摘要</span>
378
+ <p>{{ article.summary }}</p>
379
+ </div>
380
+ {% endif %}
381
+
382
+ <div class="article-content markdown-body">
383
+ {{ article.content|markdown }}
384
+ </div>
385
+ </article>
386
+
387
+ <!-- AI 聊天窗口 -->
388
+ <button class="chat-toggle" id="chatToggle">
389
+ <i class="fas fa-robot"></i>
390
+ </button>
391
+
392
+ <div class="chat-window" id="chatWindow">
393
+ <div class="chat-header">
394
+ <i class="fas fa-robot"></i>
395
+ <span class="chat-title">AI 智能助手</span>
396
+ <button class="chat-close" id="chatClose">
397
+ <i class="fas fa-times"></i>
398
+ </button>
399
+ </div>
400
+ <div class="chat-messages" id="chatMessages"></div>
401
+ <div class="chat-input-wrapper">
402
+ <textarea
403
+ id="chatInput"
404
+ class="chat-input"
405
+ placeholder="输入您的问题..."
406
+ rows="1"
407
+ ></textarea>
408
+ <button class="chat-send" onclick="sendMessage()">
409
+ <i class="fas fa-paper-plane"></i>
410
+ </button>
411
+ </div>
412
+ </div>
413
+ {% endblock %}
414
+
415
+ {% block extra_js %}
416
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
417
+ <script>
418
+ // 初始化文章上下文
419
+ window.articleContext = {
420
+ title: {{ article.title|tojson|safe }},
421
+ content: {{ article.content|tojson|safe }}
422
+ };
423
+
424
+ // 配置Marked
425
+ marked.setOptions({
426
+ breaks: true,
427
+ gfm: true
428
+ });
429
+
430
+ // 聊天窗口控制
431
+ const chatToggle = document.getElementById('chatToggle');
432
+ const chatWindow = document.getElementById('chatWindow');
433
+ const chatClose = document.getElementById('chatClose');
434
+ const chatInput = document.getElementById('chatInput');
435
+ const chatMessages = document.getElementById('chatMessages');
436
+
437
+ // 模型上下文
438
+ const modelContext = `这是一篇关于"${window.articleContext.title}"的文章。文章内容:\n\n${window.articleContext.content}\n\n请基于以上文章内容来回答用户的问题。`;
439
+
440
+ // 欢迎消息
441
+ const welcomeMessage = `您好!我是这篇《${window.articleContext.title}》的AI助手。我已经仔细阅读了全文,可以解答您关于文章内容的任何问题,也提供更深入的讨论和见解,从而帮助您更好地理解文章要点
442
+ 让我们开始对话吧!`;
443
+ // 初始化消息数组
444
+ let messages = [{
445
+ role: 'system',
446
+ content: modelContext
447
+ }];
448
+
449
+ // 初始化聊天
450
+ function initializeChat() {
451
+ displayMessage('assistant', welcomeMessage);
452
+ }
453
+
454
+ // 切换聊天窗口
455
+ function toggleChat() {
456
+ chatWindow.classList.toggle('active');
457
+ if (chatWindow.classList.contains('active')) {
458
+ chatToggle.style.display = 'none';
459
+ chatInput.focus();
460
+ if (chatMessages.children.length === 0) {
461
+ initializeChat();
462
+ }
463
+ } else {
464
+ chatToggle.style.display = 'flex';
465
+ }
466
+ }
467
+
468
+ chatToggle.addEventListener('click', toggleChat);
469
+ chatClose.addEventListener('click', toggleChat);
470
+
471
+ // 自动调整文本框高度
472
+ chatInput.addEventListener('input', function() {
473
+ this.style.height = 'auto';
474
+ this.style.height = Math.min(this.scrollHeight, 120) + 'px';
475
+ });
476
+
477
+ // 发送消息
478
+ async function sendMessage() {
479
+ const messageText = chatInput.value.trim();
480
+ if (!messageText) return;
481
+
482
+ const userMessage = {
483
+ role: 'user',
484
+ content: messageText
485
+ };
486
+
487
+ // 重置输入框
488
+ chatInput.value = '';
489
+ chatInput.style.height = 'auto';
490
+
491
+ // 显示用户消息
492
+ displayMessage('user', messageText);
493
+
494
+ try {
495
+ const currentMessages = [...messages, userMessage];
496
+
497
+ const response = await fetch('/api/chat', {
498
+ method: 'POST',
499
+ headers: {
500
+ 'Content-Type': 'application/json'
501
+ },
502
+ body: JSON.stringify({ messages: currentMessages })
503
+ });
504
+
505
+ if (response.ok) {
506
+ const data = await response.json();
507
+
508
+ // 更新消息历史
509
+ messages.push(userMessage);
510
+ messages.push({
511
+ role: 'assistant',
512
+ content: data.response
513
+ });
514
+
515
+ // 显示AI响应
516
+ displayMessage('assistant', data.response);
517
+ } else {
518
+ throw new Error('Network response was not ok');
519
+ }
520
+ } catch (error) {
521
+ console.error('Error:', error);
522
+ displayMessage('assistant', '抱歉,发生了错误,请稍后再试。');
523
+ }
524
+ }
525
+
526
+ // 显示消息
527
+ function displayMessage(role, content) {
528
+ const messageDiv = document.createElement('div');
529
+ messageDiv.className = `chat-message ${role}`;
530
+
531
+ // 使用marked渲染Markdown内容
532
+ messageDiv.innerHTML = marked.parse(content);
533
+
534
+ chatMessages.appendChild(messageDiv);
535
+ chatMessages.scrollTop = chatMessages.scrollHeight;
536
+ }
537
+
538
+ // 回车发送消息
539
+ chatInput.addEventListener('keypress', function(event) {
540
+ if (event.key === 'Enter' && !event.shiftKey) {
541
+ event.preventDefault();
542
+ sendMessage();
543
+ }
544
+ });
545
+
546
+ // 聊天窗口拖动功能
547
+ let isDragging = false;
548
+ let currentX;
549
+ let currentY;
550
+ let initialX;
551
+ let initialY;
552
+ let xOffset = 0;
553
+ let yOffset = 0;
554
+
555
+ chatWindow.addEventListener('mousedown', dragStart);
556
+ document.addEventListener('mousemove', drag);
557
+ document.addEventListener('mouseup', dragEnd);
558
+
559
+ function dragStart(e) {
560
+ if (e.target.closest('.chat-header') && !e.target.closest('.chat-close')) {
561
+ initialX = e.clientX - xOffset;
562
+ initialY = e.clientY - yOffset;
563
+ isDragging = true;
564
+ chatWindow.style.cursor = 'grabbing';
565
+ }
566
+ }
567
+
568
+ function drag(e) {
569
+ if (isDragging) {
570
+ e.preventDefault();
571
+ currentX = e.clientX - initialX;
572
+ currentY = e.clientY - initialY;
573
+ xOffset = currentX;
574
+ yOffset = currentY;
575
+
576
+ // 确保窗口不会超出视口边界
577
+ const rect = chatWindow.getBoundingClientRect();
578
+ const viewportWidth = window.innerWidth;
579
+ const viewportHeight = window.innerHeight;
580
+
581
+ // 限制X轴移动
582
+ if (rect.left < 0) {
583
+ currentX -= rect.left;
584
+ }
585
+ if (rect.right > viewportWidth) {
586
+ currentX -= (rect.right - viewportWidth);
587
+ }
588
+
589
+ // 限制Y轴移动
590
+ if (rect.top < 0) {
591
+ currentY -= rect.top;
592
+ }
593
+ if (rect.bottom > viewportHeight) {
594
+ currentY -= (rect.bottom - viewportHeight);
595
+ }
596
+
597
+ setTranslate(currentX, currentY, chatWindow);
598
+ }
599
+ }
600
+
601
+ function dragEnd() {
602
+ initialX = currentX;
603
+ initialY = currentY;
604
+ isDragging = false;
605
+ chatWindow.style.cursor = 'default';
606
+ }
607
+
608
+ function setTranslate(xPos, yPos, el) {
609
+ el.style.transform = `translate(${xPos}px, ${yPos}px)`;
610
+ }
611
+ </script>
612
+ {% endblock %}
app/templates/base.html ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
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
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --primary-blue: #6391C5;
11
+ --light-blue: #B3CFEF;
12
+ --warm-cream: #FEEEDA;
13
+ --soft-purple: #C5CDFD;
14
+ --text-dark: #2C3E50;
15
+ --bg-light: #F8FAFC;
16
+ --sidebar-width: 280px;
17
+ --header-height: 70px;
18
+ }
19
+
20
+ * {
21
+ margin: 0;
22
+ padding: 0;
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
28
+ line-height: 1.6;
29
+ color: var(--text-dark);
30
+ background-color: var(--bg-light);
31
+ min-height: 100vh;
32
+ }
33
+
34
+ /* 布局容器 */
35
+ .app-container {
36
+ display: flex;
37
+ min-height: 100vh;
38
+ }
39
+
40
+ /* 侧边栏样式 */
41
+ .sidebar {
42
+ width: var(--sidebar-width);
43
+ height: 100vh;
44
+ position: fixed;
45
+ left: 0;
46
+ top: 0;
47
+ background: white;
48
+ border-right: 2px solid rgba(99, 145, 197, 0.1);
49
+ display: flex;
50
+ flex-direction: column;
51
+ transition: transform 0.3s ease;
52
+ z-index: 100;
53
+ }
54
+
55
+ .sidebar-header {
56
+ padding: 2rem;
57
+ border-bottom: 2px solid rgba(99, 145, 197, 0.1);
58
+ }
59
+
60
+ .logo {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 1rem;
64
+ text-decoration: none;
65
+ color: var(--primary-blue);
66
+ font-size: 1.5rem;
67
+ font-weight: 600;
68
+ }
69
+
70
+ .logo i {
71
+ font-size: 2rem;
72
+ background: linear-gradient(135deg, var(--primary-blue), var(--light-blue));
73
+ -webkit-background-clip: text;
74
+ -webkit-text-fill-color: transparent;
75
+ transition: transform 0.3s ease;
76
+ }
77
+
78
+ .logo:hover i {
79
+ transform: rotate(-10deg);
80
+ }
81
+
82
+ .sidebar-content {
83
+ flex: 1;
84
+ padding: 2rem 1rem;
85
+ overflow-y: auto;
86
+ }
87
+
88
+ .nav-group {
89
+ margin-bottom: 2rem;
90
+ }
91
+
92
+ .nav-group-title {
93
+ font-size: 0.875rem;
94
+ font-weight: 600;
95
+ color: #64748B;
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.05em;
98
+ padding: 0 1rem;
99
+ margin-bottom: 1rem;
100
+ }
101
+
102
+ .nav-links {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 0.5rem;
106
+ }
107
+
108
+ .nav-link {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 1rem;
112
+ padding: 0.875rem 1rem;
113
+ color: var(--text-dark);
114
+ text-decoration: none;
115
+ border-radius: 12px;
116
+ transition: all 0.3s ease;
117
+ font-weight: 500;
118
+ position: relative;
119
+ overflow: hidden;
120
+ }
121
+
122
+ .nav-link::before {
123
+ content: '';
124
+ position: absolute;
125
+ left: 0;
126
+ top: 0;
127
+ width: 4px;
128
+ height: 100%;
129
+ background: linear-gradient(135deg, var(--primary-blue), var(--light-blue));
130
+ opacity: 0;
131
+ transition: opacity 0.3s ease;
132
+ }
133
+
134
+ .nav-link:hover {
135
+ background: linear-gradient(to right, rgba(99, 145, 197, 0.1), transparent);
136
+ }
137
+
138
+ .nav-link:hover::before {
139
+ opacity: 1;
140
+ }
141
+
142
+ .nav-link i {
143
+ font-size: 1.25rem;
144
+ color: var(--primary-blue);
145
+ transition: transform 0.3s ease;
146
+ }
147
+
148
+ .nav-link:hover i {
149
+ transform: translateX(4px);
150
+ }
151
+
152
+ .nav-link span {
153
+ font-size: 1.0625rem;
154
+ }
155
+
156
+ .sidebar-footer {
157
+ padding: 1.5rem;
158
+ border-top: 2px solid rgba(99, 145, 197, 0.1);
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 1rem;
162
+ }
163
+
164
+ /* 顶部栏样式 */
165
+ .top-header {
166
+ display: none;
167
+ position: fixed;
168
+ top: 0;
169
+ left: 0;
170
+ right: 0;
171
+ height: var(--header-height);
172
+ background: white;
173
+ border-bottom: 2px solid rgba(99, 145, 197, 0.1);
174
+ padding: 0 1.5rem;
175
+ z-index: 99;
176
+ }
177
+
178
+ .header-content {
179
+ height: 100%;
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: space-between;
183
+ }
184
+
185
+ .header-nav {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 1.5rem;
189
+ }
190
+
191
+ .menu-toggle {
192
+ display: none;
193
+ background: none;
194
+ border: none;
195
+ color: var(--primary-blue);
196
+ font-size: 1.5rem;
197
+ cursor: pointer;
198
+ padding: 0.5rem;
199
+ border-radius: 8px;
200
+ transition: all 0.3s ease;
201
+ }
202
+
203
+ .menu-toggle:hover {
204
+ background: rgba(99, 145, 197, 0.1);
205
+ }
206
+
207
+ /* 主内容区域 */
208
+ .main-content {
209
+ flex: 1;
210
+ margin-left: var(--sidebar-width);
211
+ padding: 2rem;
212
+ position: relative;
213
+ min-height: 100vh;
214
+ }
215
+
216
+ /* 响应式设计 */
217
+ @media (max-width: 768px) {
218
+ .sidebar {
219
+ transform: translateX(-100%);
220
+ }
221
+
222
+ .sidebar.active {
223
+ transform: translateX(0);
224
+ }
225
+
226
+ .top-header {
227
+ display: block;
228
+ }
229
+
230
+ .menu-toggle {
231
+ display: block;
232
+ }
233
+
234
+ .main-content {
235
+ margin-left: 0;
236
+ margin-top: var(--header-height);
237
+ padding: 1.5rem;
238
+ }
239
+ }
240
+
241
+ /* 遮罩层 */
242
+ .overlay {
243
+ display: none;
244
+ position: fixed;
245
+ top: 0;
246
+ left: 0;
247
+ right: 0;
248
+ bottom: 0;
249
+ background: rgba(0, 0, 0, 0.5);
250
+ backdrop-filter: blur(4px);
251
+ z-index: 98;
252
+ opacity: 0;
253
+ transition: opacity 0.3s ease;
254
+ }
255
+
256
+ .overlay.active {
257
+ display: block;
258
+ opacity: 1;
259
+ }
260
+
261
+ /* 滚动条美化 */
262
+ ::-webkit-scrollbar {
263
+ width: 8px;
264
+ }
265
+
266
+ ::-webkit-scrollbar-track {
267
+ background: transparent;
268
+ }
269
+
270
+ ::-webkit-scrollbar-thumb {
271
+ background: var(--light-blue);
272
+ border-radius: 4px;
273
+ }
274
+
275
+ ::-webkit-scrollbar-thumb:hover {
276
+ background: var(--primary-blue);
277
+ }
278
+ </style>
279
+ {% block extra_css %}{% endblock %}
280
+ </head>
281
+ <body>
282
+ <div class="app-container">
283
+ <!-- 顶部栏(移动端) -->
284
+ <header class="top-header">
285
+ <div class="header-content">
286
+ <button class="menu-toggle" id="menuToggle">
287
+ <i class="fas fa-bars"></i>
288
+ </button>
289
+ <a href="{{ url_for('main.index') }}" class="logo">
290
+ <i class="fas fa-feather"></i>
291
+ <span>博客</span>
292
+ </a>
293
+ <nav class="header-nav">
294
+ <a href="{{ url_for('main.index') }}" class="nav-link">
295
+ <i class="fas fa-home"></i>
296
+ <span>首页</span>
297
+ </a>
298
+ {% if session.get('logged_in') %}
299
+ <a href="{{ url_for('admin.dashboard') }}" class="nav-link">
300
+ <i class="fas fa-cog"></i>
301
+ <span>管理</span>
302
+ </a>
303
+ {% endif %}
304
+ </nav>
305
+ </div>
306
+ </header>
307
+
308
+ <!-- 遮罩层 -->
309
+ <div class="overlay" id="overlay"></div>
310
+
311
+ <!-- 侧边栏 -->
312
+ <aside class="sidebar" id="sidebar">
313
+ <div class="sidebar-header">
314
+ <a href="{{ url_for('main.index') }}" class="logo">
315
+ <i class="fas fa-feather"></i>
316
+ <span>个人博客</span>
317
+ </a>
318
+ </div>
319
+
320
+ <div class="sidebar-content">
321
+ <nav class="nav-group">
322
+ <h3 class="nav-group-title">导航</h3>
323
+ <div class="nav-links">
324
+ <a href="{{ url_for('main.index') }}" class="nav-link">
325
+ <i class="fas fa-home"></i>
326
+ <span>首页</span>
327
+ </a>
328
+ {% if session.get('logged_in') %}
329
+ <a href="{{ url_for('admin.dashboard') }}" class="nav-link">
330
+ <i class="fas fa-cog"></i>
331
+ <span>管理中心</span>
332
+ </a>
333
+ {% endif %}
334
+ </div>
335
+ </nav>
336
+
337
+ {% if session.get('logged_in') %}
338
+ <nav class="nav-group">
339
+ <h3 class="nav-group-title">内容管理</h3>
340
+ <div class="nav-links">
341
+ <a href="{{ url_for('admin.editor') }}" class="nav-link">
342
+ <i class="fas fa-edit"></i>
343
+ <span>写文章</span>
344
+ </a>
345
+ </div>
346
+ </nav>
347
+ {% endif %}
348
+ </div>
349
+
350
+ <div class="sidebar-footer">
351
+ {% if session.get('logged_in') %}
352
+ <form action="{{ url_for('admin.login') }}" method="POST" style="width: 100%;">
353
+ <button type="submit" class="nav-link" style="width: 100%; text-align: left; background: none; border: none; cursor: pointer;">
354
+ <i class="fas fa-sign-out-alt"></i>
355
+ <span>退出登录</span>
356
+ </button>
357
+ </form>
358
+ {% else %}
359
+ <a href="{{ url_for('admin.login') }}" class="nav-link">
360
+ <i class="fas fa-sign-in-alt"></i>
361
+ <span>登录</span>
362
+ </a>
363
+ {% endif %}
364
+ </div>
365
+ </aside>
366
+
367
+ <!-- 主内容区域 -->
368
+ <main class="main-content">
369
+ {% block content %}{% endblock %}
370
+ </main>
371
+ </div>
372
+
373
+ <script>
374
+ // 移动端菜单控制
375
+ const menuToggle = document.getElementById('menuToggle');
376
+ const sidebar = document.getElementById('sidebar');
377
+ const overlay = document.getElementById('overlay');
378
+
379
+ function toggleMenu() {
380
+ sidebar.classList.toggle('active');
381
+ overlay.classList.toggle('active');
382
+ document.body.style.overflow = sidebar.classList.contains('active') ? 'hidden' : '';
383
+ }
384
+
385
+ menuToggle.addEventListener('click', toggleMenu);
386
+ overlay.addEventListener('click', toggleMenu);
387
+
388
+ // 监听窗口大小变化,自动处理侧边栏状态
389
+ window.addEventListener('resize', () => {
390
+ if (window.innerWidth > 768 && sidebar.classList.contains('active')) {
391
+ sidebar.classList.remove('active');
392
+ overlay.classList.remove('active');
393
+ document.body.style.overflow = '';
394
+ }
395
+ });
396
+ </script>
397
+
398
+ {% block extra_js %}{% endblock %}
399
+ </body>
400
+ </html>
app/templates/editor.html ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{% if article %}编辑文章{% else %}新建文章{% endif %} - 个人博客{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="editor-container" style="
7
+ max-width: 1400px;
8
+ margin: 0 auto;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 1rem;
12
+ height: calc(100vh - 8rem);
13
+ padding: 0 1rem;
14
+ ">
15
+ <!-- 标题区域 -->
16
+ <div class="editor-header" style="
17
+ background: white;
18
+ border-radius: 8px;
19
+ padding: 1.25rem;
20
+ border: 1px solid #B3CFEF;
21
+ ">
22
+ <input type="text" id="titleInput" placeholder="请输入文章标题..." value="{{ article.title if article else '' }}" style="
23
+ width: 100%;
24
+ font-size: 1.5rem;
25
+ border: none;
26
+ outline: none;
27
+ color: #2C3E50;
28
+ ">
29
+ </div>
30
+
31
+ <!-- 工具栏 -->
32
+ <div class="editor-toolbar" style="
33
+ background: white;
34
+ border-radius: 8px;
35
+ padding: 0.5rem;
36
+ display: flex;
37
+ gap: 0.25rem;
38
+ flex-wrap: wrap;
39
+ border: 1px solid #B3CFEF;
40
+ "></div>
41
+
42
+ <!-- 编辑器主体 -->
43
+ <div class="editor-main" style="
44
+ display: grid;
45
+ grid-template-columns: 1fr 1fr;
46
+ gap: 1rem;
47
+ flex: 1;
48
+ min-height: 0;
49
+ ">
50
+ <!-- 编辑区域 -->
51
+ <textarea id="contentInput" placeholder="开始写作..." style="
52
+ background: white;
53
+ border-radius: 8px;
54
+ padding: 1.25rem;
55
+ border: 1px solid #B3CFEF;
56
+ font-family: 'Monaco', 'Consolas', monospace;
57
+ font-size: 0.9375rem;
58
+ line-height: 1.6;
59
+ color: #2C3E50;
60
+ resize: none;
61
+ outline: none;
62
+ ">{{ article.content if article else '' }}</textarea>
63
+
64
+ <!-- 预览区域 -->
65
+ <div id="preview" class="markdown-preview" style="
66
+ background: white;
67
+ border-radius: 8px;
68
+ padding: 1.25rem;
69
+ border: 1px solid #B3CFEF;
70
+ overflow-y: auto;
71
+ color: #2C3E50;
72
+ "></div>
73
+ </div>
74
+
75
+ <!-- 底部工具栏 -->
76
+ <div class="editor-footer" style="
77
+ background: white;
78
+ border-radius: 8px;
79
+ padding: 0.75rem 1.25rem;
80
+ display: flex;
81
+ justify-content: space-between;
82
+ align-items: center;
83
+ border: 1px solid #B3CFEF;
84
+ ">
85
+ <div class="word-count" style="
86
+ color: #6391C5;
87
+ font-size: 0.875rem;
88
+ "></div>
89
+
90
+ <button class="save-button" style="
91
+ padding: 0.5rem 1rem;
92
+ background: #6391C5;
93
+ color: white;
94
+ border: none;
95
+ border-radius: 4px;
96
+ font-size: 0.875rem;
97
+ cursor: pointer;
98
+ transition: background-color 0.2s;
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 0.5rem;
102
+ ">
103
+ <i class="fas fa-save" style="font-size: 0.875rem;"></i>
104
+ 保存文章
105
+ </button>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- 图片上传输入框 -->
110
+ <input type="file" id="imageInput" style="display: none" accept="image/*">
111
+
112
+ <style>
113
+ /* 工具栏按钮基础样式 */
114
+ .toolbar-button {
115
+ width: 32px;
116
+ height: 32px;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ border: 1px solid transparent;
121
+ border-radius: 4px;
122
+ color: #6391C5;
123
+ background: none;
124
+ cursor: pointer;
125
+ transition: all 0.2s;
126
+ font-size: 0.9375rem;
127
+ }
128
+
129
+ .toolbar-button:hover {
130
+ background: #f8fafc;
131
+ border-color: #B3CFEF;
132
+ }
133
+
134
+ /* 预览区域样式优化 */
135
+ .markdown-preview h1,
136
+ .markdown-preview h2,
137
+ .markdown-preview h3 {
138
+ margin-top: 1.5em;
139
+ margin-bottom: 1em;
140
+ color: #2C3E50;
141
+ }
142
+
143
+ .markdown-preview code {
144
+ background: #f8fafc;
145
+ padding: 0.2em 0.4em;
146
+ border-radius: 4px;
147
+ font-size: 0.9em;
148
+ color: #6391C5;
149
+ }
150
+
151
+ .markdown-preview pre {
152
+ background: #f8fafc;
153
+ padding: 1rem;
154
+ border-radius: 4px;
155
+ border: 1px solid #B3CFEF;
156
+ overflow-x: auto;
157
+ }
158
+
159
+ .markdown-preview pre code {
160
+ background: none;
161
+ padding: 0;
162
+ border-radius: 0;
163
+ }
164
+
165
+ .markdown-preview blockquote {
166
+ border-left: 4px solid #B3CFEF;
167
+ margin: 1em 0;
168
+ padding-left: 1em;
169
+ color: #64748b;
170
+ }
171
+
172
+ .markdown-preview img {
173
+ max-width: 100%;
174
+ border-radius: 4px;
175
+ }
176
+ </style>
177
+
178
+ <script>
179
+ // 编辑区域焦点效果
180
+ document.getElementById('contentInput').addEventListener('focus', function() {
181
+ this.style.borderColor = '#6391C5';
182
+ });
183
+
184
+ document.getElementById('contentInput').addEventListener('blur', function() {
185
+ this.style.borderColor = '#B3CFEF';
186
+ });
187
+
188
+ // 标题输入框焦点效果
189
+ document.getElementById('titleInput').addEventListener('focus', function() {
190
+ this.style.borderColor = '#6391C5';
191
+ });
192
+
193
+ document.getElementById('titleInput').addEventListener('blur', function() {
194
+ this.style.borderColor = '#B3CFEF';
195
+ });
196
+
197
+ // 保存按钮悬停效果
198
+ document.querySelector('.save-button').addEventListener('mouseenter', function() {
199
+ this.style.background = '#5682b6';
200
+ });
201
+
202
+ document.querySelector('.save-button').addEventListener('mouseleave', function() {
203
+ this.style.background = '#6391C5';
204
+ });
205
+ </script>
206
+ {% endblock %}
207
+
208
+ {% block extra_js %}
209
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
210
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
211
+ <script src="{{ url_for('static', filename='js/editor.js') }}"></script>
212
+ {% endblock %}
app/templates/index.html ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}首页 - 个人博客{% endblock %}
4
+
5
+ {% block content %}
6
+ <!-- 搜索区域 -->
7
+ <section class="search-container">
8
+ <div class="search-wrapper">
9
+ <input
10
+ type="text"
11
+ class="search-input"
12
+ placeholder="搜索文章..."
13
+ id="searchInput"
14
+ >
15
+ <i class="fas fa-search search-icon"></i>
16
+ <button class="search-reset" id="searchReset" style="display: none;">
17
+ <i class="fas fa-times"></i>
18
+ </button>
19
+ </div>
20
+ </section>
21
+
22
+ <!-- 文章列表 -->
23
+ <section class="articles-section">
24
+ <div class="section-header">
25
+ <h1 class="section-title">所有文章</h1>
26
+ <div class="view-controls">
27
+ <button class="view-btn" data-view="list">
28
+ <i class="fas fa-list"></i>
29
+ 列表视图
30
+ </button>
31
+ <button class="view-btn active" data-view="grid">
32
+ <i class="fas fa-th-large"></i>
33
+ 卡片视图
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="articles-container grid-view" id="articlesContainer">
39
+ {% for article in articles %}
40
+ <article class="article-card">
41
+ <div class="article-icon">
42
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
43
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
44
+ <polyline points="14 2 14 8 20 8"></polyline>
45
+ <line x1="16" y1="13" x2="8" y2="13"></line>
46
+ <line x1="16" y1="17" x2="8" y2="17"></line>
47
+ <line x1="10" y1="9" x2="8" y2="9"></line>
48
+ </svg>
49
+ </div>
50
+ <div class="article-content">
51
+ <h2 class="article-title">
52
+ <a href="{{ url_for('main.article', slug=article.slug) }}">
53
+ {{ article.title }}
54
+ </a>
55
+ </h2>
56
+ {% if article.summary %}
57
+ <div class="article-summary">
58
+ {{ article.summary }}
59
+ </div>
60
+ {% endif %}
61
+ <div class="article-meta">
62
+ <span class="meta-date">
63
+ <i class="fas fa-calendar"></i>
64
+ {{ article.created_at.strftime('%Y-%m-%d') }}
65
+ </span>
66
+ </div>
67
+ </div>
68
+ </article>
69
+ {% endfor %}
70
+ </div>
71
+ </section>
72
+
73
+ <style>
74
+ .search-container {
75
+ margin-bottom: 2rem;
76
+ }
77
+
78
+ .search-wrapper {
79
+ position: relative;
80
+ max-width: 800px;
81
+ margin: 0 auto;
82
+ }
83
+
84
+ .search-input {
85
+ width: 100%;
86
+ padding: 1rem 1.25rem 1rem 3rem;
87
+ border: 2px solid var(--light-blue);
88
+ border-radius: 12px;
89
+ background: white;
90
+ font-size: 1rem;
91
+ transition: all 0.3s ease;
92
+ color: var(--text-dark);
93
+ padding-right: 3rem;
94
+ }
95
+
96
+ .search-input:focus {
97
+ outline: none;
98
+ border-color: var(--primary-blue);
99
+ box-shadow: 0 0 0 4px rgba(99, 145, 197, 0.1);
100
+ }
101
+
102
+ .search-icon {
103
+ position: absolute;
104
+ left: 1.25rem;
105
+ top: 50%;
106
+ transform: translateY(-50%);
107
+ color: var(--primary-blue);
108
+ }
109
+
110
+ .search-reset {
111
+ position: absolute;
112
+ right: 1rem;
113
+ top: 50%;
114
+ transform: translateY(-50%);
115
+ background: none;
116
+ border: none;
117
+ color: var(--primary-blue);
118
+ cursor: pointer;
119
+ padding: 0.5rem;
120
+ border-radius: 50%;
121
+ transition: all 0.3s ease;
122
+ }
123
+
124
+ .search-reset:hover {
125
+ background: rgba(99, 145, 197, 0.1);
126
+ }
127
+
128
+ .section-header {
129
+ margin-bottom: 2rem;
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: center;
133
+ }
134
+
135
+ .section-title {
136
+ font-size: 1.75rem;
137
+ color: var(--text-dark);
138
+ font-weight: 600;
139
+ }
140
+
141
+ .view-controls {
142
+ display: flex;
143
+ gap: 0.5rem;
144
+ background: white;
145
+ padding: 0.25rem;
146
+ border-radius: 8px;
147
+ border: 1px solid var(--light-blue);
148
+ }
149
+
150
+ .view-btn {
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 0.5rem;
154
+ padding: 0.5rem 1rem;
155
+ border: none;
156
+ background: none;
157
+ border-radius: 6px;
158
+ color: var(--text-dark);
159
+ cursor: pointer;
160
+ transition: all 0.3s ease;
161
+ }
162
+
163
+ .view-btn.active {
164
+ background: var(--primary-blue);
165
+ color: white;
166
+ }
167
+
168
+ .view-btn i {
169
+ font-size: 1rem;
170
+ }
171
+
172
+ /* 列表视图样式 */
173
+ .articles-container.list-view {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 1rem;
177
+ }
178
+
179
+ .list-view .article-card {
180
+ background: white;
181
+ border-radius: 12px;
182
+ border: 1px solid var(--light-blue);
183
+ padding: 1.5rem;
184
+ display: flex;
185
+ gap: 1.5rem;
186
+ transition: all 0.3s ease;
187
+ }
188
+
189
+ /* 网格视图样式 */
190
+ .articles-container.grid-view {
191
+ display: grid;
192
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
193
+ gap: 1.5rem;
194
+ }
195
+
196
+ .grid-view .article-card {
197
+ background: white;
198
+ border-radius: 12px;
199
+ border: 1px solid var(--light-blue);
200
+ padding: 1.5rem;
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 1rem;
204
+ transition: all 0.3s ease;
205
+ }
206
+
207
+ .grid-view .article-icon {
208
+ align-self: flex-start;
209
+ }
210
+
211
+ .article-card:hover {
212
+ border-color: var(--primary-blue);
213
+ box-shadow: 0 4px 12px rgba(99, 145, 197, 0.1);
214
+ transform: translateY(-2px);
215
+ }
216
+
217
+ .article-icon {
218
+ color: var(--primary-blue);
219
+ flex-shrink: 0;
220
+ }
221
+
222
+ .article-content {
223
+ flex: 1;
224
+ }
225
+
226
+ .article-title {
227
+ font-size: 1.25rem;
228
+ font-weight: 600;
229
+ margin-bottom: 0.75rem;
230
+ line-height: 1.4;
231
+ }
232
+
233
+ .article-title a {
234
+ color: var(--text-dark);
235
+ text-decoration: none;
236
+ transition: color 0.3s ease;
237
+ }
238
+
239
+ .article-title a:hover {
240
+ color: var(--primary-blue);
241
+ }
242
+
243
+ .article-summary {
244
+ color: #64748B;
245
+ margin-bottom: 1rem;
246
+ line-height: 1.6;
247
+ display: -webkit-box;
248
+ -webkit-line-clamp: 2;
249
+ -webkit-box-orient: vertical;
250
+ overflow: hidden;
251
+ }
252
+
253
+ .article-meta {
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 1rem;
257
+ color: #94A3B8;
258
+ font-size: 0.875rem;
259
+ }
260
+
261
+ .meta-date {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 0.5rem;
265
+ }
266
+
267
+ .meta-date i {
268
+ color: var(--primary-blue);
269
+ }
270
+
271
+ @media (max-width: 640px) {
272
+ .article-card {
273
+ padding: 1rem;
274
+ }
275
+
276
+ .article-summary {
277
+ -webkit-line-clamp: 3;
278
+ }
279
+
280
+ .articles-container.grid-view {
281
+ grid-template-columns: 1fr;
282
+ }
283
+
284
+ .section-header {
285
+ flex-direction: column;
286
+ gap: 1rem;
287
+ align-items: flex-start;
288
+ }
289
+ }
290
+ </style>
291
+
292
+ <script>
293
+ const searchInput = document.getElementById('searchInput');
294
+ const searchReset = document.getElementById('searchReset');
295
+ const articlesContainer = document.getElementById('articlesContainer');
296
+ const viewButtons = document.querySelectorAll('.view-btn');
297
+ let allArticles = [...document.querySelectorAll('.article-card')];
298
+
299
+ function debounce(func, wait) {
300
+ let timeout;
301
+ return function executedFunction(...args) {
302
+ const later = () => {
303
+ clearTimeout(timeout);
304
+ func(...args);
305
+ };
306
+ clearTimeout(timeout);
307
+ timeout = setTimeout(later, wait);
308
+ };
309
+ }
310
+
311
+ function filterArticles(query) {
312
+ query = query.toLowerCase().trim();
313
+ searchReset.style.display = query ? 'block' : 'none';
314
+
315
+ allArticles.forEach(article => {
316
+ const title = article.querySelector('.article-title').textContent.toLowerCase();
317
+ const summary = article.querySelector('.article-summary')?.textContent.toLowerCase() || '';
318
+
319
+ if (title.includes(query) || summary.includes(query) || query === '') {
320
+ article.style.display = '';
321
+ } else {
322
+ article.style.display = 'none';
323
+ }
324
+ });
325
+ }
326
+
327
+ // 视图切换
328
+ viewButtons.forEach(button => {
329
+ button.addEventListener('click', () => {
330
+ viewButtons.forEach(btn => btn.classList.remove('active'));
331
+ button.classList.add('active');
332
+
333
+ const viewType = button.dataset.view;
334
+ articlesContainer.className = `articles-container ${viewType}-view`;
335
+ });
336
+ });
337
+
338
+ // 搜索功能
339
+ const debouncedFilter = debounce(filterArticles, 300);
340
+
341
+ searchInput.addEventListener('input', (e) => {
342
+ debouncedFilter(e.target.value);
343
+ });
344
+
345
+ // 重置搜索
346
+ searchReset.addEventListener('click', () => {
347
+ searchInput.value = '';
348
+ filterArticles('');
349
+ searchInput.focus();
350
+ });
351
+ </script>
352
+ {% endblock %}