mistpe commited on
Commit
a0c6f58
·
verified ·
1 Parent(s): 13d72d1

Upload 4 files

Browse files
app/templates/article.html CHANGED
@@ -1,612 +1,612 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}{{ article.title }} - Wisdom Hub{% 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 %}
 
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 CHANGED
@@ -313,7 +313,7 @@
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
 
 
313
  <div class="sidebar-header">
314
  <a href="{{ url_for('main.index') }}" class="logo">
315
  <i class="fas fa-feather"></i>
316
+ <span>Wisdom Hub</span>
317
  </a>
318
  </div>
319
 
app/templates/editor.html CHANGED
@@ -1,212 +1,212 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}{% if article %}编辑文章{% else %}新建文章{% endif %} - Wisdom Hub{% 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 %}
 
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 CHANGED
@@ -1,352 +1,352 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}首页 - Wisdom Hub{% 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 %}
 
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 %}