You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

804 lines
28 KiB

// 收藏网站管理类
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 => `
<div class="category-item ${category.id === this.currentCategory ? 'active' : ''}"
data-id="${category.id}"
style="border-color: ${category.color}">
${category.name}
</div>
`).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 => `
<div class="tag-item ${tag.id === this.currentTag ? 'active' : ''}"
data-id="${tag.id}"
style="border-color: ${tag.color}">
${tag.name}
</div>
`).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 =>
`<span class="bookmark-tag">${tag}</span>`
).join('');
const linksHtml = bookmark.links.length > 0 ?
`<div class="bookmark-links">
<small>额外链接: ${bookmark.links.length}个</small>
</div>` : '';
return `
<div class="bookmark-card" data-id="${bookmark.id}">
<div class="bookmark-header">
<img src="${bookmark.favicon}" alt="favicon" class="bookmark-favicon"
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22><rect width=%2232%22 height=%2232%22 fill=%22%23666%22 rx=%224%22/></svg>'">
<h3 class="bookmark-title">${bookmark.title}</h3>
<button class="bookmark-favorite ${bookmark.is_favorite ? 'active' : ''}"
data-id="${bookmark.id}" title="特别收藏">
${bookmark.is_favorite ? '★' : '☆'}
</button>
</div>
<p class="bookmark-description">${bookmark.description}</p>
<div class="bookmark-meta">
<span class="bookmark-category" style="background: ${bookmark.category_color}20; color: ${bookmark.category_color}">
${bookmark.category_name}
</span>
<div class="bookmark-stats">
<span>👁 ${bookmark.click_count}</span>
<span>📅 ${this.formatDate(bookmark.created_at)}</span>
</div>
</div>
${tagsHtml ? `<div class="bookmark-tags">${tagsHtml}</div>` : ''}
${linksHtml}
<div class="bookmark-actions">
<button class="bookmark-btn primary" onclick="window.open('${bookmark.url}', '_blank')">
访问网站
</button>
<button class="bookmark-btn" onclick="bookmarkManager.editBookmark(${bookmark.id})">
编辑
</button>
<button class="bookmark-btn" onclick="bookmarkManager.deleteBookmark(${bookmark.id})">
删除
</button>
</div>
</div>
`;
}
createBookmarkListItem(bookmark) {
const tagsHtml = bookmark.tags.map(tag =>
`<span class="bookmark-tag">${tag}</span>`
).join('');
return `
<div class="bookmark-list-item" data-id="${bookmark.id}">
<img src="${bookmark.favicon}" alt="favicon" class="bookmark-list-favicon"
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22><rect width=%2224%22 height=%2224%22 fill=%22%23666%22 rx=%223%22/></svg>'">
<div class="bookmark-list-content">
<h4 class="bookmark-list-title">${bookmark.title}</h4>
<p class="bookmark-list-description">${bookmark.description}</p>
${tagsHtml ? `<div class="bookmark-tags">${tagsHtml}</div>` : ''}
</div>
<div class="bookmark-list-meta">
<span class="bookmark-category" style="background: ${bookmark.category_color}20; color: ${bookmark.category_color}">
${bookmark.category_name}
</span>
<span>👁 ${bookmark.click_count}</span>
<button class="bookmark-btn" onclick="bookmarkManager.editBookmark(${bookmark.id})">编辑</button>
<button class="bookmark-btn" onclick="bookmarkManager.deleteBookmark(${bookmark.id})">删除</button>
</div>
</div>
`;
}
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 += `
<button class="pagination-btn" ${!this.pagination.hasPrev ? 'disabled' : ''}
onclick="bookmarkManager.goToPage(${this.currentPage - 1})">
上一页
</button>
`;
// 页码
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 += `
<button class="pagination-btn ${i === this.currentPage ? 'active' : ''}"
onclick="bookmarkManager.goToPage(${i})">
${i}
</button>
`;
} else if (i === this.currentPage - 3 || i === this.currentPage + 3) {
paginationHtml += '<span class="pagination-btn">...</span>';
}
}
// 下一页
paginationHtml += `
<button class="pagination-btn" ${!this.pagination.hasNext ? 'disabled' : ''}
onclick="bookmarkManager.goToPage(${this.currentPage + 1})">
下一页
</button>
`;
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 = '<option value="">选择分类</option>' +
this.categories.map(category =>
`<option value="${category.id}">${category.name}</option>`
).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 = `
<input type="text" placeholder="链接标题" class="link-title" value="${title}">
<input type="url" placeholder="链接URL" class="link-url" value="${url}">
<button type="button" class="remove-link">删除</button>
`;
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 = `
<div class="empty-state">
<h3>暂无收藏</h3>
<p>开始添加你的第一个收藏吧!</p>
<button class="empty-btn" onclick="bookmarkManager.showAddModal()">
添加收藏
</button>
</div>
`;
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();
}
}