mistpe commited on
Commit
d82405d
·
verified ·
1 Parent(s): 00ee81d

Upload 2 files

Browse files
Files changed (2) hide show
  1. app/static/js/editor.js +427 -0
  2. app/static/js/main.js +218 -0
app/static/js/editor.js ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class MarkdownEditor {
2
+ constructor() {
3
+ this.initializeElements();
4
+ this.initializeEventListeners();
5
+ this.initializeAutoSave();
6
+ this.initializeToolbar();
7
+ this.initializeDragAndDrop();
8
+ this.initializeLocalBackup();
9
+ this.lastSaveTime = null;
10
+ }
11
+
12
+ initializeElements() {
13
+ this.titleInput = document.getElementById('titleInput');
14
+ this.contentInput = document.getElementById('contentInput');
15
+ this.preview = document.getElementById('preview');
16
+ this.imageInput = document.getElementById('imageInput');
17
+ this.saveButton = document.querySelector('.save-button');
18
+ this.wordCount = document.querySelector('.word-count');
19
+ this.toolbar = document.querySelector('.editor-toolbar');
20
+
21
+ this.isDirty = false;
22
+ this.lastSavedContent = '';
23
+ this.autoSaveInterval = null;
24
+
25
+ // Clear localStorage if this is a new article
26
+ const isNewArticle = window.location.pathname.endsWith('/editor');
27
+ if (isNewArticle) {
28
+ localStorage.removeItem('editor-content');
29
+ localStorage.removeItem('editor-title');
30
+ // Clear inputs for new article
31
+ this.titleInput.value = '';
32
+ this.contentInput.value = '';
33
+ }
34
+ }
35
+
36
+ initializeEventListeners() {
37
+ this.contentInput.addEventListener('input', () => {
38
+ this.handleContentChange();
39
+ this.updateWordCount();
40
+ this.isDirty = true;
41
+ });
42
+
43
+ this.titleInput.addEventListener('input', () => {
44
+ this.isDirty = true;
45
+ });
46
+
47
+ this.imageInput.addEventListener('change', (event) => {
48
+ this.handleImageUpload(event);
49
+ });
50
+
51
+ this.contentInput.addEventListener('keydown', (event) => {
52
+ this.handleShortcuts(event);
53
+ });
54
+
55
+ this.saveButton.addEventListener('click', () => {
56
+ this.saveArticle();
57
+ });
58
+
59
+ window.addEventListener('beforeunload', (event) => {
60
+ if (this.isDirty) {
61
+ event.preventDefault();
62
+ event.returnValue = '您有未保存的更改,确定要离开吗?';
63
+ }
64
+ });
65
+ }
66
+
67
+ handleContentChange() {
68
+ this.updatePreview();
69
+ }
70
+
71
+ updatePreview() {
72
+ try {
73
+ const content = this.contentInput.value;
74
+ const markedInstance = marked.parse || marked;
75
+ this.preview.innerHTML = markedInstance(content, {
76
+ breaks: true,
77
+ gfm: true,
78
+ highlight: function(code, lang) {
79
+ if (lang && hljs.getLanguage(lang)) {
80
+ return hljs.highlight(code, { language: lang }).value;
81
+ }
82
+ return code;
83
+ }
84
+ });
85
+
86
+ this.preview.querySelectorAll('pre code').forEach(block => {
87
+ hljs.highlightElement(block);
88
+ });
89
+ } catch (error) {
90
+ console.error('预览渲染错误:', error);
91
+ this.preview.innerHTML = '<div class="error">预览渲染失败</div>';
92
+ }
93
+ }
94
+
95
+ initializeAutoSave() {
96
+ this.autoSaveInterval = setInterval(() => {
97
+ if (this.isDirty && this.lastSaveTime &&
98
+ (Date.now() - this.lastSaveTime) >= 600000) { // 10 minutes
99
+ this.autoSave();
100
+ }
101
+ }, 60000); // Check every minute
102
+ }
103
+
104
+ async autoSave() {
105
+ if (!this.isDirty) return;
106
+
107
+ const content = this.contentInput.value;
108
+ const title = this.titleInput.value;
109
+
110
+ if (content === this.lastSavedContent || !title.trim() || !content.trim()) {
111
+ return;
112
+ }
113
+
114
+ try {
115
+ const response = await this.saveArticle(true);
116
+ if (response && response.ok) {
117
+ this.lastSavedContent = content;
118
+ this.showNotification('自动保存成功', 'success');
119
+ }
120
+ } catch (error) {
121
+ console.error('自动保存失败:', error);
122
+ this.showNotification('自动保存失败', 'error');
123
+ }
124
+ }
125
+
126
+ insertText(before, after, defaultText = '') {
127
+ const start = this.contentInput.selectionStart;
128
+ const end = this.contentInput.selectionEnd;
129
+ const content = this.contentInput.value;
130
+
131
+ const selectedText = content.substring(start, end) || defaultText;
132
+ const replacement = before + selectedText + after;
133
+
134
+ this.contentInput.value = content.substring(0, start) +
135
+ replacement +
136
+ content.substring(end);
137
+
138
+ this.contentInput.focus();
139
+ const newCursorPos = start + before.length + selectedText.length;
140
+ this.contentInput.setSelectionRange(newCursorPos, newCursorPos);
141
+
142
+ this.updatePreview();
143
+ this.isDirty = true;
144
+ }
145
+
146
+ async handleImageUpload(event) {
147
+ const file = event.target.files[0];
148
+ if (!file) return;
149
+
150
+ if (!file.type.startsWith('image/')) {
151
+ this.showNotification('请选择图片文件', 'error');
152
+ return;
153
+ }
154
+
155
+ const formData = new FormData();
156
+ formData.append('file', file);
157
+
158
+ try {
159
+ this.showNotification('正在上传图片...', 'info');
160
+ const response = await fetch('/api/upload', {
161
+ method: 'POST',
162
+ body: formData
163
+ });
164
+
165
+ const data = await response.json();
166
+
167
+ if (data.url) {
168
+ this.insertText(`![${file.name}](${data.url})`, '');
169
+ this.showNotification('图片上传成功', 'success');
170
+ } else {
171
+ throw new Error(data.error || '上传失败');
172
+ }
173
+ } catch (error) {
174
+ console.error('图片上传错误:', error);
175
+ this.showNotification('图片上传失败', 'error');
176
+ }
177
+ }
178
+
179
+ handleShortcuts(event) {
180
+ if (event.ctrlKey || event.metaKey) {
181
+ switch (event.key.toLowerCase()) {
182
+ case 's':
183
+ event.preventDefault();
184
+ this.saveArticle();
185
+ break;
186
+ case 'b':
187
+ event.preventDefault();
188
+ this.insertText('**', '**', '粗体文本');
189
+ break;
190
+ case 'i':
191
+ event.preventDefault();
192
+ this.insertText('*', '*', '斜体文本');
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ async saveArticle(isAutoSave = false) {
199
+ const title = this.titleInput.value.trim();
200
+ const content = this.contentInput.value.trim();
201
+ this.lastSaveTime = Date.now();
202
+
203
+ if (!title || !content) {
204
+ this.showNotification('标题和内容不能为空', 'error');
205
+ return;
206
+ }
207
+
208
+ const articleSlug = window.location.pathname.split('/').pop();
209
+ const isEdit = articleSlug !== 'editor';
210
+
211
+ try {
212
+ if (!isAutoSave) this.showNotification('正在保存...', 'info');
213
+
214
+ const response = await fetch(`/api/articles${isEdit ? '/' + articleSlug : ''}`, {
215
+ method: isEdit ? 'PUT' : 'POST',
216
+ headers: {
217
+ 'Content-Type': 'application/json'
218
+ },
219
+ body: JSON.stringify({
220
+ title,
221
+ content
222
+ })
223
+ });
224
+
225
+ const data = await response.json();
226
+
227
+ if (response.ok) {
228
+ this.isDirty = false;
229
+ if (!isAutoSave) {
230
+ this.showNotification('保存成功', 'success');
231
+ window.location.href = `/article/${data.slug || articleSlug}`;
232
+ }
233
+ return response;
234
+ } else {
235
+ throw new Error(data.error || '保存失败');
236
+ }
237
+ } catch (error) {
238
+ console.error('保存文章错误:', error);
239
+ this.showNotification(error.message, 'error');
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ updateWordCount() {
245
+ const content = this.contentInput.value;
246
+ const wordCount = content.length;
247
+ if (this.wordCount) {
248
+ this.wordCount.textContent = `字数:${wordCount}`;
249
+ }
250
+ }
251
+
252
+ showNotification(message, type = 'info') {
253
+ const notification = document.createElement('div');
254
+ notification.className = `notification ${type}`;
255
+ notification.innerHTML = `
256
+ <div class="notification-content">
257
+ <span class="notification-message">${message}</span>
258
+ <button class="notification-close">&times;</button>
259
+ </div>
260
+ `;
261
+
262
+ document.body.appendChild(notification);
263
+
264
+ const closeButton = notification.querySelector('.notification-close');
265
+ closeButton.addEventListener('click', () => {
266
+ notification.remove();
267
+ });
268
+
269
+ setTimeout(() => {
270
+ notification.classList.add('fade-out');
271
+ setTimeout(() => {
272
+ notification.remove();
273
+ }, 300);
274
+ }, 3000);
275
+ }
276
+
277
+ initializeToolbar() {
278
+ const tools = [
279
+ {
280
+ name: 'bold',
281
+ icon: '<i class="fas fa-bold"></i>',
282
+ title: '粗体 (Ctrl+B)',
283
+ action: () => this.insertText('**', '**', '粗体文本')
284
+ },
285
+ {
286
+ name: 'italic',
287
+ icon: '<i class="fas fa-italic"></i>',
288
+ title: '斜体 (Ctrl+I)',
289
+ action: () => this.insertText('*', '*', '斜体文本')
290
+ },
291
+ {
292
+ name: 'heading1',
293
+ icon: '<i class="fas fa-heading"></i>',
294
+ title: '一级标题',
295
+ action: () => this.insertText('\n# ', '', '标题')
296
+ },
297
+ {
298
+ name: 'heading2',
299
+ icon: '<i class="fas fa-heading fa-sm"></i>',
300
+ title: '二级标题',
301
+ action: () => this.insertText('\n## ', '', '标题')
302
+ },
303
+ {
304
+ name: 'code',
305
+ icon: '<i class="fas fa-code"></i>',
306
+ title: '代码块',
307
+ action: () => this.insertText('\n```\n', '\n```\n', '在此输入代码')
308
+ },
309
+ {
310
+ name: 'link',
311
+ icon: '<i class="fas fa-link"></i>',
312
+ title: '链接',
313
+ action: () => this.insertText('[', '](https://)', '链接文本')
314
+ },
315
+ {
316
+ name: 'image',
317
+ icon: '<i class="fas fa-image"></i>',
318
+ title: '图片',
319
+ action: () => this.imageInput.click()
320
+ },
321
+ {
322
+ name: 'list',
323
+ icon: '<i class="fas fa-list-ul"></i>',
324
+ title: '无序列表',
325
+ action: () => this.insertText('\n- ', '', '列表项')
326
+ },
327
+ {
328
+ name: 'numbered-list',
329
+ icon: '<i class="fas fa-list-ol"></i>',
330
+ title: '有序列表',
331
+ action: () => this.insertText('\n1. ', '', '列表项')
332
+ },
333
+ {
334
+ name: 'quote',
335
+ icon: '<i class="fas fa-quote-right"></i>',
336
+ title: '引用',
337
+ action: () => this.insertText('\n> ', '', '引用文本')
338
+ },
339
+ {
340
+ name: 'divider',
341
+ icon: '<i class="fas fa-minus"></i>',
342
+ title: '分隔线',
343
+ action: () => this.insertText('\n---\n', '', '')
344
+ }
345
+ ];
346
+
347
+ tools.forEach(tool => {
348
+ const button = document.createElement('button');
349
+ button.className = `toolbar-button ${tool.name}`;
350
+ button.innerHTML = tool.icon;
351
+ button.title = tool.title;
352
+ button.addEventListener('click', (e) => {
353
+ e.preventDefault();
354
+ tool.action();
355
+ });
356
+ this.toolbar.appendChild(button);
357
+ });
358
+ }
359
+
360
+ initializeDragAndDrop() {
361
+ const dropZone = this.contentInput;
362
+
363
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
364
+ dropZone.addEventListener(eventName, (e) => {
365
+ e.preventDefault();
366
+ e.stopPropagation();
367
+ });
368
+ });
369
+
370
+ ['dragenter', 'dragover'].forEach(eventName => {
371
+ dropZone.addEventListener(eventName, () => {
372
+ dropZone.classList.add('drag-over');
373
+ });
374
+ });
375
+
376
+ ['dragleave', 'drop'].forEach(eventName => {
377
+ dropZone.addEventListener(eventName, () => {
378
+ dropZone.classList.remove('drag-over');
379
+ });
380
+ });
381
+
382
+ dropZone.addEventListener('drop', (e) => {
383
+ const files = e.dataTransfer.files;
384
+ if (files.length > 0 && files[0].type.startsWith('image/')) {
385
+ this.imageInput.files = files;
386
+ this.handleImageUpload({ target: this.imageInput });
387
+ }
388
+ });
389
+ }
390
+
391
+ initializeLocalBackup() {
392
+ // Only restore from local storage if we're editing an existing article
393
+ const isNewArticle = window.location.pathname.endsWith('/editor');
394
+ if (!isNewArticle) {
395
+ const savedContent = localStorage.getItem('editor-content');
396
+ const savedTitle = localStorage.getItem('editor-title');
397
+
398
+ if (savedContent && !this.contentInput.value) {
399
+ this.contentInput.value = savedContent;
400
+ this.updatePreview();
401
+ }
402
+
403
+ if (savedTitle && !this.titleInput.value) {
404
+ this.titleInput.value = savedTitle;
405
+ }
406
+ }
407
+
408
+ // Save to local storage periodically
409
+ setInterval(() => {
410
+ if (this.isDirty) {
411
+ localStorage.setItem('editor-content', this.contentInput.value);
412
+ localStorage.setItem('editor-title', this.titleInput.value);
413
+ }
414
+ }, 10000);
415
+ }
416
+
417
+ destroy() {
418
+ clearInterval(this.autoSaveInterval);
419
+ localStorage.removeItem('editor-content');
420
+ localStorage.removeItem('editor-title');
421
+ }
422
+ }
423
+
424
+ // Initialize editor when the DOM is ready
425
+ document.addEventListener('DOMContentLoaded', () => {
426
+ const editor = new MarkdownEditor();
427
+ });
app/static/js/main.js ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 全局工具函数和初始化
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // 等待 marked 库加载完成
4
+ setTimeout(() => {
5
+ // 初始化搜索功能
6
+ initializeSearch();
7
+ // 初始化Markdown渲染
8
+ initializeMarkdownRendering();
9
+ // 初始化返回顶部按钮
10
+ initializeScrollToTop();
11
+ // 初始化移动端导航菜单
12
+ initializeMobileNavigation();
13
+ }, 100);
14
+ });
15
+
16
+ // 搜索功能实现
17
+ function initializeSearch() {
18
+ const searchInput = document.querySelector('.search-input');
19
+ const searchResults = document.querySelector('.search-results');
20
+
21
+ if (searchInput) {
22
+ searchInput.addEventListener('input', debounce(async (event) => {
23
+ const searchTerm = event.target.value.trim().toLowerCase();
24
+ if (searchTerm.length < 2) {
25
+ searchResults.style.display = 'none';
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const response = await fetch(`/api/search?q=${encodeURIComponent(searchTerm)}`);
31
+ const data = await response.json();
32
+
33
+ if (data.articles.length > 0) {
34
+ displaySearchResults(data.articles, searchResults);
35
+ } else {
36
+ searchResults.innerHTML = '<div class="no-results">没有找到相关文章</div>';
37
+ }
38
+ searchResults.style.display = 'block';
39
+ } catch (error) {
40
+ console.error('搜索出错:', error);
41
+ searchResults.innerHTML = '<div class="error">搜索服务暂时不可用</div>';
42
+ }
43
+ }, 300));
44
+
45
+ // 点击其他区域关闭搜索结果
46
+ document.addEventListener('click', (event) => {
47
+ if (!event.target.closest('.search-container')) {
48
+ searchResults.style.display = 'none';
49
+ }
50
+ });
51
+ }
52
+ }
53
+
54
+ // Markdown渲染初始化
55
+ function initializeMarkdownRendering() {
56
+ if (typeof marked === 'undefined') {
57
+ console.error('Marked library not loaded');
58
+ return;
59
+ }
60
+
61
+ // 配置 marked
62
+ marked.use({
63
+ breaks: true,
64
+ gfm: true
65
+ });
66
+
67
+ const markdownElements = document.querySelectorAll('.markdown-body');
68
+
69
+ markdownElements.forEach(element => {
70
+ // 检查元素是否已经被渲染过
71
+ if (element.dataset.rendered) {
72
+ return;
73
+ }
74
+
75
+ try {
76
+ // 标记为已渲染
77
+ element.dataset.rendered = 'true';
78
+
79
+ // 如果内容是通过 Flask 的 markdown 过滤器渲染的,就不需要再次渲染
80
+ if (!element.classList.contains('server-rendered')) {
81
+ const content = element.textContent;
82
+ element.innerHTML = marked(content);
83
+ }
84
+
85
+ // 处理代码块
86
+ element.querySelectorAll('pre code').forEach(block => {
87
+ if (typeof hljs !== 'undefined') {
88
+ hljs.highlightElement(block);
89
+ }
90
+ });
91
+
92
+ // 为图片添加点击放大功能
93
+ element.querySelectorAll('img').forEach(img => {
94
+ img.addEventListener('click', () => {
95
+ openImageViewer(img.src);
96
+ });
97
+ });
98
+ } catch (error) {
99
+ console.error('Markdown渲染错误:', error);
100
+ }
101
+ });
102
+ }
103
+
104
+ // 返回顶部按钮实现
105
+ function initializeScrollToTop() {
106
+ const scrollTopButton = document.createElement('button');
107
+ scrollTopButton.className = 'scroll-top-button';
108
+ scrollTopButton.innerHTML = '↑';
109
+ document.body.appendChild(scrollTopButton);
110
+
111
+ window.addEventListener('scroll', debounce(() => {
112
+ if (window.scrollY > 500) {
113
+ scrollTopButton.classList.add('visible');
114
+ } else {
115
+ scrollTopButton.classList.remove('visible');
116
+ }
117
+ }, 100));
118
+
119
+ scrollTopButton.addEventListener('click', () => {
120
+ window.scrollTo({
121
+ top: 0,
122
+ behavior: 'smooth'
123
+ });
124
+ });
125
+ }
126
+
127
+ // 移动端导航菜单实现
128
+ function initializeMobileNavigation() {
129
+ const menuButton = document.querySelector('.menu-button');
130
+ const navLinks = document.querySelector('.nav-links');
131
+
132
+ if (menuButton && navLinks) {
133
+ menuButton.addEventListener('click', () => {
134
+ navLinks.classList.toggle('active');
135
+ menuButton.classList.toggle('active');
136
+ });
137
+
138
+ // 点击导航链接后关闭菜单
139
+ navLinks.querySelectorAll('a').forEach(link => {
140
+ link.addEventListener('click', () => {
141
+ navLinks.classList.remove('active');
142
+ menuButton.classList.remove('active');
143
+ });
144
+ });
145
+ }
146
+ }
147
+
148
+ // 图片查看器实��
149
+ function openImageViewer(imageSrc) {
150
+ const viewer = document.createElement('div');
151
+ viewer.className = 'image-viewer';
152
+ viewer.innerHTML = `
153
+ <div class="image-viewer-content">
154
+ <img src="${imageSrc}" alt="预览图片">
155
+ <button class="close-button">×</button>
156
+ </div>
157
+ `;
158
+
159
+ viewer.addEventListener('click', (event) => {
160
+ if (event.target === viewer || event.target.className === 'close-button') {
161
+ viewer.remove();
162
+ }
163
+ });
164
+
165
+ document.body.appendChild(viewer);
166
+ }
167
+
168
+ // 工具函数: 防抖
169
+ function debounce(func, wait) {
170
+ let timeout;
171
+ return function executedFunction(...args) {
172
+ const later = () => {
173
+ clearTimeout(timeout);
174
+ func(...args);
175
+ };
176
+ clearTimeout(timeout);
177
+ timeout = setTimeout(later, wait);
178
+ };
179
+ }
180
+
181
+ // 工具函数: 搜索结果显示
182
+ function displaySearchResults(articles, container) {
183
+ container.innerHTML = articles.map(article => `
184
+ <a href="/article/${article.slug}" class="search-result-item">
185
+ <h3>${highlightSearchTerm(article.title)}</h3>
186
+ ${article.summary ? `<p>${highlightSearchTerm(article.summary)}</p>` : ''}
187
+ <span class="article-date">${formatDate(article.created_at)}</span>
188
+ </a>
189
+ `).join('');
190
+ }
191
+
192
+ // 工具函数: 高亮搜索词
193
+ function highlightSearchTerm(text, searchTerm) {
194
+ if (!searchTerm) return text;
195
+ const regex = new RegExp(searchTerm, 'gi');
196
+ return text.replace(regex, match => `<mark>${match}</mark>`);
197
+ }
198
+
199
+ // 工具函数: 日期格式化
200
+ function formatDate(dateString) {
201
+ const date = new Date(dateString);
202
+ return date.toLocaleDateString('zh-CN', {
203
+ year: 'numeric',
204
+ month: '2-digit',
205
+ day: '2-digit'
206
+ });
207
+ }
208
+
209
+ // 错误处理函数
210
+ function handleError(error, container) {
211
+ console.error('发生错误:', error);
212
+ container.innerHTML = `
213
+ <div class="error-message">
214
+ <p>抱歉,发生了一些错误</p>
215
+ <button onclick="window.location.reload()">刷新页面</button>
216
+ </div>
217
+ `;
218
+ }