// 收藏网站管理类 class BookmarkManager { constructor() { this.currentView = 'grid'; this.currentPage = 1; this.itemsPerPage = 12; this.currentCategory = null; this.currentTag = null; this.searchQuery = ''; this.bookmarks = []; this.categories = []; this.tags = []; this.editingBookmark = null; this.totalBookmarks = 0; this.pagination = null; this.init(); } async init() { this.bindEvents(); await this.loadInitialData(); this.renderUI(); } bindEvents() { // 搜索功能 document.getElementById('searchBtn').addEventListener('click', () => this.handleSearch()); document.getElementById('searchInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') this.handleSearch(); }); // 添加收藏 document.getElementById('addBookmarkBtn').addEventListener('click', () => this.showAddModal()); // 视图切换 document.getElementById('gridViewBtn').addEventListener('click', () => this.switchView('grid')); document.getElementById('listViewBtn').addEventListener('click', () => this.switchView('list')); // 模态框 document.getElementById('closeModal').addEventListener('click', () => this.closeModal()); document.getElementById('bookmarkForm').addEventListener('submit', (e) => this.handleFormSubmit(e)); document.getElementById('addLinkBtn').addEventListener('click', () => this.addExtraLink()); // 点击模态框外部关闭 document.querySelector('.modal-overlay').addEventListener('click', (e) => { if (e.target.classList.contains('modal-overlay')) { this.closeModal(); } }); } async loadInitialData() { try { // 调用后端API加载数据 await this.loadCategories(); await this.loadTags(); await this.loadBookmarks(); } catch (error) { console.error('加载数据失败:', error); this.showError('加载数据失败,请刷新页面重试'); } } async loadCategories() { try { const response = await fetch('/categories', { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { throw new Error('获取分类失败'); } const result = await response.json(); if (result.success) { this.categories = result.data; } else { throw new Error(result.message || '获取分类失败'); } } catch (error) { console.error('加载分类失败:', error); // 如果API调用失败,使用默认分类 this.categories = [ { id: 1, name: '技术开发', color: '#3B82F6', icon: 'code' }, { id: 2, name: '学习资源', color: '#10B981', icon: 'graduation-cap' }, { id: 3, name: '设计工具', color: '#F59E0B', icon: 'palette' }, { id: 4, name: '效率工具', color: '#8B5CF6', icon: 'zap' }, { id: 5, name: '娱乐休闲', color: '#EC4899', icon: 'heart' } ]; } } async loadTags() { try { const response = await fetch('/tags/popular', { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { throw new Error('获取标签失败'); } const result = await response.json(); if (result.success) { this.tags = result.data; } else { throw new Error(result.message || '获取标签失败'); } } catch (error) { console.error('加载标签失败:', error); // 如果API调用失败,使用默认标签 this.tags = [ { id: 1, name: 'JavaScript', color: '#F7DF1E' }, { id: 2, name: 'React', color: '#61DAFB' }, { id: 3, name: 'Node.js', color: '#339933' }, { id: 4, name: 'CSS', color: '#1572B6' }, { id: 5, name: '设计灵感', color: '#FF6B6B' }, { id: 6, name: '免费资源', color: '#4ECDC4' }, { id: 7, name: 'API', color: '#45B7D1' }, { id: 8, name: '数据库', color: '#FFA500' } ]; } } async loadBookmarks() { try { const params = new URLSearchParams({ page: this.currentPage, limit: this.itemsPerPage }); if (this.currentCategory) { params.append('category_id', this.currentCategory); } if (this.currentTag) { params.append('tag_id', this.currentTag); } if (this.searchQuery) { params.append('search', this.searchQuery); } const response = await fetch(`/bookmarks?${params}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { throw new Error('获取收藏失败'); } const result = await response.json(); if (result.success) { this.bookmarks = result.data; this.totalBookmarks = result.total; this.pagination = result.pagination; } else { throw new Error(result.message || '获取收藏失败'); } } catch (error) { console.error('加载收藏失败:', error); // 如果API调用失败,使用默认数据 this.bookmarks = [ { id: 1, title: 'MDN Web Docs', description: 'Mozilla开发者网络,提供Web技术文档和教程', url: 'https://developer.mozilla.org/', favicon: 'https://developer.mozilla.org/favicon-48x48.cbbd161b.png', category_id: 1, category_name: '技术开发', category_color: '#3B82F6', is_favorite: true, click_count: 15, tags: ['JavaScript', 'Node.js', 'CSS'], links: [ { title: 'JavaScript 教程', url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript', type: 'tutorial' }, { title: 'CSS 参考', url: 'https://developer.mozilla.org/zh-CN/docs/Web/CSS', type: 'reference' } ], created_at: '2024-01-15T10:30:00Z' } ]; } } renderUI() { this.renderCategories(); this.renderTags(); this.renderBookmarks(); this.renderPagination(); } renderCategories() { const categoryList = document.getElementById('categoryList'); const allCategories = [{ id: null, name: '全部', color: '#6B7280', icon: 'grid' }, ...this.categories]; categoryList.innerHTML = allCategories.map(category => `
${category.name}
`).join(''); // 绑定分类点击事件 categoryList.querySelectorAll('.category-item').forEach(item => { item.addEventListener('click', () => { const categoryId = item.dataset.id ? parseInt(item.dataset.id) : null; this.filterByCategory(categoryId); }); }); } renderTags() { const tagList = document.getElementById('tagList'); const allTags = [{ id: null, name: '全部', color: '#6B7280' }, ...this.tags]; tagList.innerHTML = allTags.map(tag => `
${tag.name}
`).join(''); // 绑定标签点击事件 tagList.querySelectorAll('.tag-item').forEach(item => { item.addEventListener('click', () => { const tagId = item.dataset.id ? parseInt(item.dataset.id) : null; this.filterByTag(tagId); }); }); } renderBookmarks() { if (this.bookmarks.length === 0) { this.showEmptyState(); return; } if (this.currentView === 'grid') { this.renderGridView(this.bookmarks); } else { this.renderListView(this.bookmarks); } } renderGridView(bookmarks) { const gridContainer = document.getElementById('bookmarksGrid'); const listContainer = document.getElementById('bookmarksList'); gridContainer.style.display = 'grid'; listContainer.style.display = 'none'; gridContainer.innerHTML = bookmarks.map(bookmark => this.createBookmarkCard(bookmark)).join(''); this.bindBookmarkEvents(); } renderListView(bookmarks) { const gridContainer = document.getElementById('bookmarksGrid'); const listContainer = document.getElementById('bookmarksList'); gridContainer.style.display = 'none'; listContainer.style.display = 'block'; listContainer.innerHTML = bookmarks.map(bookmark => this.createBookmarkListItem(bookmark)).join(''); this.bindBookmarkEvents(); } createBookmarkCard(bookmark) { const tagsHtml = bookmark.tags.map(tag => `${tag}` ).join(''); const linksHtml = bookmark.links.length > 0 ? `` : ''; return `
favicon

${bookmark.title}

${bookmark.description}

${bookmark.category_name}
👁 ${bookmark.click_count} 📅 ${this.formatDate(bookmark.created_at)}
${tagsHtml ? `
${tagsHtml}
` : ''} ${linksHtml}
`; } createBookmarkListItem(bookmark) { const tagsHtml = bookmark.tags.map(tag => `${tag}` ).join(''); return `
favicon

${bookmark.title}

${bookmark.description}

${tagsHtml ? `
${tagsHtml}
` : ''}
${bookmark.category_name} 👁 ${bookmark.click_count}
`; } bindBookmarkEvents() { // 绑定收藏按钮事件 document.querySelectorAll('.bookmark-favorite').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const bookmarkId = parseInt(btn.dataset.id); this.toggleFavorite(bookmarkId); }); }); } renderPagination() { if (!this.pagination || this.pagination.totalPages <= 1) { document.getElementById('pagination').innerHTML = ''; return; } let paginationHtml = ''; // 上一页 paginationHtml += ` `; // 页码 for (let i = 1; i <= this.pagination.totalPages; i++) { if (i === 1 || i === this.pagination.totalPages || (i >= this.currentPage - 2 && i <= this.currentPage + 2)) { paginationHtml += ` `; } else if (i === this.currentPage - 3 || i === this.currentPage + 3) { paginationHtml += '...'; } } // 下一页 paginationHtml += ` `; document.getElementById('pagination').innerHTML = paginationHtml; } getFilteredBookmarks() { // 现在数据直接从后端获取,已经过滤过了 return this.bookmarks; } async handleSearch() { this.searchQuery = document.getElementById('searchInput').value.trim(); this.currentPage = 1; await this.loadBookmarks(); this.renderUI(); } async filterByCategory(categoryId) { this.currentCategory = categoryId; this.currentPage = 1; await this.loadBookmarks(); this.renderUI(); } async filterByTag(tagId) { this.currentTag = tagId; this.currentPage = 1; await this.loadBookmarks(); this.renderUI(); } switchView(view) { this.currentView = view; // 更新按钮状态 document.querySelectorAll('.view-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.view === view); }); this.renderBookmarks(); } async goToPage(page) { this.currentPage = page; await this.loadBookmarks(); this.renderUI(); // 滚动到顶部 document.querySelector('.bookmarks-section').scrollIntoView({ behavior: 'smooth' }); } showAddModal() { this.editingBookmark = null; document.getElementById('modalTitle').textContent = '添加收藏'; document.getElementById('bookmarkForm').reset(); this.populateCategorySelect(); this.showModal(); } editBookmark(bookmarkId) { const bookmark = this.bookmarks.find(b => b.id === bookmarkId); if (!bookmark) return; this.editingBookmark = bookmark; document.getElementById('modalTitle').textContent = '编辑收藏'; // 填充表单 document.getElementById('bookmarkTitle').value = bookmark.title; document.getElementById('bookmarkUrl').value = bookmark.url; document.getElementById('bookmarkDescription').value = bookmark.description; document.getElementById('bookmarkCategory').value = bookmark.category_id; document.getElementById('bookmarkTags').value = bookmark.tags.join(', '); this.populateCategorySelect(); this.populateExtraLinks(bookmark.links); this.showModal(); } async handleFormSubmit(e) { e.preventDefault(); const formData = new FormData(e.target); const bookmarkData = { title: formData.get('bookmarkTitle'), url: formData.get('bookmarkUrl'), description: formData.get('bookmarkDescription'), category_id: formData.get('bookmarkCategory') ? parseInt(formData.get('bookmarkCategory')) : null, tags: formData.get('bookmarkTags').split(',').map(tag => tag.trim()).filter(tag => tag), links: this.getExtraLinksData() }; try { if (this.editingBookmark) { await this.updateBookmark(this.editingBookmark.id, bookmarkData); } else { await this.createBookmark(bookmarkData); } this.closeModal(); this.renderUI(); this.showSuccess(this.editingBookmark ? '收藏更新成功' : '收藏添加成功'); } catch (error) { this.showError(error.message); } } async createBookmark(data) { try { const response = await fetch('/bookmarks', { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(data) }); if (!response.ok) { throw new Error('创建收藏失败'); } const result = await response.json(); if (result.success) { const newBookmark = result.data; this.bookmarks.unshift(newBookmark); return newBookmark; } else { throw new Error(result.message || '创建收藏失败'); } } catch (error) { console.error('创建收藏失败:', error); throw error; } } async updateBookmark(id, data) { try { const response = await fetch(`/bookmarks/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(data) }); if (!response.ok) { throw new Error('更新收藏失败'); } const result = await response.json(); if (result.success) { const updatedBookmark = result.data; const bookmarkIndex = this.bookmarks.findIndex(b => b.id === id); if (bookmarkIndex !== -1) { this.bookmarks[bookmarkIndex] = updatedBookmark; } return updatedBookmark; } else { throw new Error(result.message || '更新收藏失败'); } } catch (error) { console.error('更新收藏失败:', error); throw error; } } async deleteBookmark(id) { if (!confirm('确定要删除这个收藏吗?')) return; try { const response = await fetch(`/bookmarks/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { throw new Error('删除收藏失败'); } const result = await response.json(); if (result.success) { const bookmarkIndex = this.bookmarks.findIndex(b => b.id === id); if (bookmarkIndex !== -1) { this.bookmarks.splice(bookmarkIndex, 1); } this.renderUI(); this.showSuccess('收藏删除成功'); } else { throw new Error(result.message || '删除收藏失败'); } } catch (error) { console.error('删除收藏失败:', error); this.showError(error.message); } } async toggleFavorite(id) { try { const response = await fetch(`/bookmarks/${id}/favorite`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { throw new Error('切换收藏状态失败'); } const result = await response.json(); if (result.success) { const bookmark = this.bookmarks.find(b => b.id === id); if (bookmark) { bookmark.is_favorite = result.data.is_favorite; } this.renderUI(); this.showSuccess(result.message); } else { throw new Error(result.message || '切换收藏状态失败'); } } catch (error) { console.error('切换收藏状态失败:', error); this.showError(error.message); } } populateCategorySelect() { const select = document.getElementById('bookmarkCategory'); select.innerHTML = '' + this.categories.map(category => `` ).join(''); } populateExtraLinks(links) { const container = document.getElementById('extraLinks'); container.innerHTML = ''; if (links && links.length > 0) { links.forEach(link => this.addExtraLink(link.title, link.url)); } else { this.addExtraLink(); } } addExtraLink(title = '', url = '') { const container = document.getElementById('extraLinks'); const linkItem = document.createElement('div'); linkItem.className = 'extra-link-item'; linkItem.innerHTML = ` `; linkItem.querySelector('.remove-link').addEventListener('click', () => { if (container.children.length > 1) { container.removeChild(linkItem); } }); container.appendChild(linkItem); } getExtraLinksData() { const links = []; document.querySelectorAll('.extra-link-item').forEach(item => { const title = item.querySelector('.link-title').value.trim(); const url = item.querySelector('.link-url').value.trim(); if (title && url) { links.push({ title, url, type: 'link' }); } }); return links; } showModal() { document.getElementById('bookmarkModal').style.display = 'block'; document.body.style.overflow = 'hidden'; } closeModal() { document.getElementById('bookmarkModal').style.display = 'none'; document.body.style.overflow = ''; this.editingBookmark = null; } showEmptyState() { const gridContainer = document.getElementById('bookmarksGrid'); const listContainer = document.getElementById('bookmarksList'); gridContainer.style.display = 'none'; listContainer.style.display = 'none'; const emptyHtml = `

暂无收藏

开始添加你的第一个收藏吧!

`; if (this.currentView === 'grid') { gridContainer.innerHTML = emptyHtml; gridContainer.style.display = 'block'; } else { listContainer.innerHTML = emptyHtml; listContainer.style.display = 'block'; } } showSuccess(message) { this.showToast(message, 'success'); } showError(message) { this.showToast(message, 'error'); } showToast(message, type = 'info') { // 创建toast元素 const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'success' ? '#10B981' : type === 'error' ? '#EF4444' : '#3B82F6'}; color: white; padding: 12px 20px; border-radius: 8px; z-index: 10000; animation: slideIn 0.3s ease; `; document.body.appendChild(toast); // 3秒后自动移除 setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, 3000); } formatDate(dateString) { const date = new Date(dateString); const now = new Date(); const diffTime = Math.abs(now - date); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); if (diffDays === 1) return '今天'; if (diffDays === 2) return '昨天'; if (diffDays <= 7) return `${diffDays - 1}天前`; if (diffDays <= 30) return `${Math.floor(diffDays / 7)}周前`; if (diffDays <= 365) return `${Math.floor(diffDays / 30)}个月前`; return `${Math.floor(diffDays / 365)}年前`; } } // 添加CSS动画 const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style); // 初始化收藏管理器 let bookmarkManager; document.addEventListener('DOMContentLoaded', () => { bookmarkManager = new BookmarkManager(); }); // 全局函数 function closeModal() { if (bookmarkManager) { bookmarkManager.closeModal(); } }