1025 lines
35 KiB
JavaScript
1025 lines
35 KiB
JavaScript
// 网址管理器应用
|
||
class UrlManager {
|
||
constructor() {
|
||
this.urls = [];
|
||
this.categories = [];
|
||
this.selectedUrls = new Set();
|
||
this.currentCategory = 'all';
|
||
this.currentSearch = '';
|
||
this.currentTag = '';
|
||
this.viewMode = 'grid';
|
||
this.editingUrlId = null;
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.loadData();
|
||
this.setupEventListeners();
|
||
this.render();
|
||
this.setupKeyboardShortcuts();
|
||
}
|
||
|
||
// 数据存储
|
||
loadData() {
|
||
// 加载默认数据或从localStorage加载
|
||
const savedUrls = localStorage.getItem('urlManager_urls');
|
||
const savedCategories = localStorage.getItem('urlManager_categories');
|
||
|
||
if (savedUrls) {
|
||
this.urls = JSON.parse(savedUrls);
|
||
} else {
|
||
// 添加一些示例数据
|
||
this.urls = this.getDefaultUrls();
|
||
}
|
||
|
||
if (savedCategories) {
|
||
this.categories = JSON.parse(savedCategories);
|
||
} else {
|
||
this.categories = this.getDefaultCategories();
|
||
}
|
||
}
|
||
|
||
saveData() {
|
||
try {
|
||
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
|
||
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
|
||
} catch (error) {
|
||
console.error('保存数据到localStorage失败:', error);
|
||
// 如果localStorage已满或其他错误,只记录错误,不抛出异常
|
||
// 因为数据已经在内存中了,只是无法持久化
|
||
// 抛出异常会导致误报"保存失败"
|
||
}
|
||
}
|
||
|
||
getDefaultUrls() {
|
||
return [
|
||
{
|
||
id: '1',
|
||
title: 'Android开发者文档',
|
||
url: 'https://developer.android.com',
|
||
description: '官方Android开发文档和API参考',
|
||
category: 'dev-docs',
|
||
icon: 'fas fa-book',
|
||
tags: ['文档', '官方', 'Android'],
|
||
favorite: true,
|
||
createdAt: new Date().toISOString(),
|
||
lastAccessed: null,
|
||
accessCount: 0
|
||
},
|
||
{
|
||
id: '2',
|
||
title: 'GitHub',
|
||
url: 'https://github.com',
|
||
description: '代码托管和版本控制平台',
|
||
category: 'tools',
|
||
icon: 'fas fa-code',
|
||
tags: ['工具', '代码', '版本控制'],
|
||
favorite: true,
|
||
createdAt: new Date().toISOString(),
|
||
lastAccessed: null,
|
||
accessCount: 0
|
||
},
|
||
{
|
||
id: '3',
|
||
title: 'Stack Overflow',
|
||
url: 'https://stackoverflow.com',
|
||
description: '开发者问答社区',
|
||
category: 'community',
|
||
icon: 'fas fa-comments',
|
||
tags: ['社区', '问答', '帮助'],
|
||
favorite: false,
|
||
createdAt: new Date().toISOString(),
|
||
lastAccessed: null,
|
||
accessCount: 0
|
||
},
|
||
{
|
||
id: '4',
|
||
title: 'Material Design',
|
||
url: 'https://material.io',
|
||
description: 'Google的设计系统',
|
||
category: 'design',
|
||
icon: 'fas fa-palette',
|
||
tags: ['设计', 'UI', 'Material'],
|
||
favorite: false,
|
||
createdAt: new Date().toISOString(),
|
||
lastAccessed: null,
|
||
accessCount: 0
|
||
}
|
||
];
|
||
}
|
||
|
||
getDefaultCategories() {
|
||
return [
|
||
{ id: 'dev-docs', name: '开发文档', color: '#3498db', icon: 'fas fa-book' },
|
||
{ id: 'tools', name: '工具', color: '#2ecc71', icon: 'fas fa-tools' },
|
||
{ id: 'community', name: '社区', color: '#9b59b6', icon: 'fas fa-comments' },
|
||
{ id: 'design', name: '设计', color: '#e74c3c', icon: 'fas fa-palette' }
|
||
];
|
||
}
|
||
|
||
// 网址管理方法
|
||
addUrl(urlData) {
|
||
const newUrl = {
|
||
id: Date.now().toString(),
|
||
createdAt: new Date().toISOString(),
|
||
lastAccessed: null,
|
||
accessCount: 0,
|
||
...urlData
|
||
};
|
||
|
||
// 先保存数据到内存和localStorage(核心操作,不能失败)
|
||
this.urls.push(newUrl);
|
||
this.saveData();
|
||
|
||
// 如果当前有分类过滤或搜索过滤,切换到"全部网址"以确保新添加的网址能显示
|
||
if (this.currentCategory !== 'all' || this.currentSearch || this.currentTag) {
|
||
this.currentCategory = 'all';
|
||
this.currentSearch = '';
|
||
this.currentTag = '';
|
||
// 清除搜索框
|
||
const searchInput = document.getElementById('searchInput');
|
||
if (searchInput) {
|
||
searchInput.value = '';
|
||
}
|
||
}
|
||
|
||
// 渲染页面(必须执行,确保页面刷新)
|
||
// render() 会更新分类选择器的UI状态,所以不需要单独更新
|
||
this.render();
|
||
return newUrl;
|
||
}
|
||
|
||
updateUrl(id, updates) {
|
||
const index = this.urls.findIndex(url => url.id === id);
|
||
if (index !== -1) {
|
||
// 先保存数据到内存和localStorage(核心操作,不能失败)
|
||
this.urls[index] = { ...this.urls[index], ...updates };
|
||
this.saveData();
|
||
|
||
// 渲染页面(必须执行,确保页面刷新)
|
||
this.render();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
deleteUrl(id) {
|
||
const index = this.urls.findIndex(url => url.id === id);
|
||
if (index !== -1) {
|
||
this.urls.splice(index, 1);
|
||
this.selectedUrls.delete(id);
|
||
this.saveData();
|
||
this.render();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
deleteSelectedUrls() {
|
||
this.urls = this.urls.filter(url => !this.selectedUrls.has(url.id));
|
||
this.selectedUrls.clear();
|
||
this.saveData();
|
||
this.render();
|
||
this.hideBulkActions();
|
||
}
|
||
|
||
toggleFavorite(id) {
|
||
const url = this.urls.find(url => url.id === id);
|
||
if (url) {
|
||
url.favorite = !url.favorite;
|
||
this.saveData();
|
||
this.render();
|
||
}
|
||
}
|
||
|
||
// 分类管理
|
||
addCategory(categoryData) {
|
||
const newCategory = {
|
||
id: Date.now().toString(),
|
||
...categoryData
|
||
};
|
||
|
||
this.categories.push(newCategory);
|
||
this.saveData();
|
||
this.renderCategories();
|
||
return newCategory;
|
||
}
|
||
|
||
// 过滤和搜索
|
||
getFilteredUrls() {
|
||
let filtered = [...this.urls];
|
||
|
||
// 按分类过滤
|
||
if (this.currentCategory === 'favorites') {
|
||
filtered = filtered.filter(url => url.favorite);
|
||
} else if (this.currentCategory === 'recent') {
|
||
filtered = filtered.filter(url => url.lastAccessed)
|
||
.sort((a, b) => new Date(b.lastAccessed) - new Date(a.lastAccessed))
|
||
.slice(0, 20);
|
||
} else if (this.currentCategory !== 'all') {
|
||
filtered = filtered.filter(url => url.category === this.currentCategory);
|
||
}
|
||
|
||
// 按搜索词过滤
|
||
if (this.currentSearch) {
|
||
const searchLower = this.currentSearch.toLowerCase();
|
||
filtered = filtered.filter(url =>
|
||
url.title.toLowerCase().includes(searchLower) ||
|
||
url.url.toLowerCase().includes(searchLower) ||
|
||
url.description?.toLowerCase().includes(searchLower) ||
|
||
url.tags?.some(tag => tag.toLowerCase().includes(searchLower))
|
||
);
|
||
}
|
||
|
||
// 按标签过滤
|
||
if (this.currentTag) {
|
||
filtered = filtered.filter(url =>
|
||
url.tags?.some(tag => tag.toLowerCase() === this.currentTag.toLowerCase())
|
||
);
|
||
}
|
||
|
||
return filtered;
|
||
}
|
||
|
||
// 渲染方法
|
||
render() {
|
||
// 分别处理每个渲染方法,确保即使某个方法失败,其他方法也能执行
|
||
try {
|
||
this.renderCategories();
|
||
} catch (error) {
|
||
console.warn('渲染分类时出错:', error);
|
||
}
|
||
|
||
try {
|
||
this.renderUrls();
|
||
} catch (error) {
|
||
console.warn('渲染网址列表时出错:', error);
|
||
}
|
||
|
||
try {
|
||
this.renderStats();
|
||
} catch (error) {
|
||
console.warn('渲染统计信息时出错:', error);
|
||
}
|
||
|
||
try {
|
||
this.renderTags();
|
||
} catch (error) {
|
||
console.warn('渲染标签时出错:', error);
|
||
}
|
||
}
|
||
|
||
renderCategories() {
|
||
const categoriesList = document.getElementById('categoriesList');
|
||
if (!categoriesList) return;
|
||
|
||
const allCount = this.urls.length;
|
||
const favoritesCount = this.urls.filter(url => url.favorite).length;
|
||
const recentCount = this.urls.filter(url => url.lastAccessed).length;
|
||
|
||
let html = `
|
||
<div class="category-item ${this.currentCategory === 'all' ? 'active' : ''}" data-id="all">
|
||
<i class="fas fa-globe"></i>
|
||
<span>全部网址</span>
|
||
<span class="count-badge">${allCount}</span>
|
||
</div>
|
||
<div class="category-item ${this.currentCategory === 'favorites' ? 'active' : ''}" data-id="favorites">
|
||
<i class="fas fa-star"></i>
|
||
<span>收藏夹</span>
|
||
<span class="count-badge">${favoritesCount}</span>
|
||
</div>
|
||
<div class="category-item ${this.currentCategory === 'recent' ? 'active' : ''}" data-id="recent">
|
||
<i class="fas fa-history"></i>
|
||
<span>最近访问</span>
|
||
<span class="count-badge">${recentCount}</span>
|
||
</div>
|
||
`;
|
||
|
||
// 添加自定义分类
|
||
this.categories.forEach(category => {
|
||
const count = this.urls.filter(url => url.category === category.id).length;
|
||
html += `
|
||
<div class="category-item ${this.currentCategory === category.id ? 'active' : ''}" data-id="${category.id}">
|
||
<i class="${category.icon}" style="color: ${category.color}"></i>
|
||
<span>${category.name}</span>
|
||
<span class="count-badge">${count}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
categoriesList.innerHTML = html;
|
||
|
||
// 更新分类选择器
|
||
this.updateCategorySelector();
|
||
}
|
||
|
||
renderUrls() {
|
||
const urlsContainer = document.getElementById('urlsContainer');
|
||
const emptyState = document.getElementById('emptyState');
|
||
if (!urlsContainer) {
|
||
console.warn('urlsContainer 元素不存在');
|
||
return;
|
||
}
|
||
|
||
const filteredUrls = this.getFilteredUrls();
|
||
|
||
if (filteredUrls.length === 0) {
|
||
if (emptyState) {
|
||
emptyState.style.display = 'block';
|
||
}
|
||
urlsContainer.innerHTML = '';
|
||
if (emptyState) {
|
||
urlsContainer.appendChild(emptyState);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (emptyState) {
|
||
emptyState.style.display = 'none';
|
||
}
|
||
|
||
let html = '';
|
||
filteredUrls.forEach(url => {
|
||
const category = this.categories.find(c => c.id === url.category);
|
||
const isSelected = this.selectedUrls.has(url.id);
|
||
|
||
html += `
|
||
<div class="url-card ${isSelected ? 'selected' : ''}" data-id="${url.id}">
|
||
<input type="checkbox" ${isSelected ? 'checked' : ''}>
|
||
<div class="url-card-header">
|
||
<div style="display: flex; align-items: center;">
|
||
<i class="${url.icon} url-icon"></i>
|
||
<div>
|
||
<div class="url-title">${url.title}</div>
|
||
<div class="url-address">${url.url}</div>
|
||
</div>
|
||
</div>
|
||
<div class="url-actions">
|
||
<button class="favorite-btn ${url.favorite ? 'active' : ''}" title="${url.favorite ? '取消收藏' : '收藏'}">
|
||
<i class="fas fa-star"></i>
|
||
</button>
|
||
<button class="edit-url" title="编辑">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="delete-url" title="删除">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
${url.description ? `<div class="url-description">${url.description}</div>` : ''}
|
||
<div class="url-footer">
|
||
<div class="url-tags">
|
||
${url.tags?.map(tag => `<span class="url-tag">${tag}</span>`).join('') || ''}
|
||
</div>
|
||
<div class="url-meta">
|
||
${category ? `<span style="color: ${category.color}"><i class="${category.icon}"></i> ${category.name}</span>` : ''}
|
||
<button class="open-url" title="打开网址">
|
||
<i class="fas fa-external-link-alt"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
urlsContainer.innerHTML = html;
|
||
}
|
||
|
||
renderStats() {
|
||
const totalUrlsEl = document.getElementById('totalUrls');
|
||
const totalCategoriesEl = document.getElementById('totalCategories');
|
||
const totalFavoritesEl = document.getElementById('totalFavorites');
|
||
|
||
if (totalUrlsEl) totalUrlsEl.textContent = this.urls.length;
|
||
if (totalCategoriesEl) totalCategoriesEl.textContent = this.categories.length;
|
||
if (totalFavoritesEl) totalFavoritesEl.textContent = this.urls.filter(url => url.favorite).length;
|
||
}
|
||
|
||
renderTags() {
|
||
const tagsContainer = document.getElementById('tagsContainer');
|
||
if (!tagsContainer) return;
|
||
|
||
// 收集所有标签
|
||
const tagCounts = {};
|
||
this.urls.forEach(url => {
|
||
url.tags?.forEach(tag => {
|
||
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
||
});
|
||
});
|
||
|
||
const tags = Object.entries(tagCounts)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 10);
|
||
|
||
let html = '';
|
||
tags.forEach(([tag, count]) => {
|
||
const isActive = this.currentTag === tag;
|
||
html += `<span class="tag ${isActive ? 'active' : ''}" data-tag="${tag}">${tag} (${count})</span>`;
|
||
});
|
||
|
||
tagsContainer.innerHTML = html;
|
||
}
|
||
|
||
updateCategorySelector() {
|
||
try {
|
||
const categorySelect = document.getElementById('urlCategory');
|
||
if (!categorySelect) return;
|
||
|
||
let html = '<option value="">选择分类</option>';
|
||
this.categories.forEach(category => {
|
||
html += `<option value="${category.id}">${category.name}</option>`;
|
||
});
|
||
|
||
categorySelect.innerHTML = html;
|
||
} catch (error) {
|
||
console.warn('更新分类选择器时出错:', error);
|
||
// 不影响其他操作
|
||
}
|
||
}
|
||
|
||
// 事件监听器
|
||
setupEventListeners() {
|
||
// 主题切换
|
||
document.getElementById('themeToggle').addEventListener('click', () => {
|
||
this.toggleTheme();
|
||
});
|
||
|
||
// 添加网址按钮
|
||
document.getElementById('addUrlBtn').addEventListener('click', () => {
|
||
this.showUrlModal();
|
||
});
|
||
|
||
document.getElementById('addFirstUrlBtn').addEventListener('click', () => {
|
||
this.showUrlModal();
|
||
});
|
||
|
||
// 添加分类按钮
|
||
document.getElementById('addCategoryBtn').addEventListener('click', () => {
|
||
this.showCategoryModal();
|
||
});
|
||
|
||
// 搜索
|
||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||
this.currentSearch = e.target.value;
|
||
this.renderUrls();
|
||
});
|
||
|
||
document.getElementById('clearSearch').addEventListener('click', () => {
|
||
document.getElementById('searchInput').value = '';
|
||
this.currentSearch = '';
|
||
this.renderUrls();
|
||
});
|
||
|
||
// 视图切换
|
||
document.getElementById('gridViewBtn').addEventListener('click', () => {
|
||
this.setViewMode('grid');
|
||
});
|
||
|
||
document.getElementById('listViewBtn').addEventListener('click', () => {
|
||
this.setViewMode('list');
|
||
});
|
||
|
||
// 批量操作
|
||
document.getElementById('bulkActionsBtn').addEventListener('click', () => {
|
||
this.showBulkActions();
|
||
});
|
||
|
||
document.getElementById('bulkCancelBtn').addEventListener('click', () => {
|
||
this.hideBulkActions();
|
||
});
|
||
|
||
document.getElementById('bulkDeleteBtn').addEventListener('click', () => {
|
||
if (confirm(`确定要删除选中的 ${this.selectedUrls.size} 个网址吗?`)) {
|
||
this.deleteSelectedUrls();
|
||
}
|
||
});
|
||
|
||
document.getElementById('bulkOpenBtn').addEventListener('click', () => {
|
||
this.openSelectedUrls();
|
||
});
|
||
|
||
// 导入/导出
|
||
document.getElementById('importBtn').addEventListener('click', () => {
|
||
this.showImportExportModal('import');
|
||
});
|
||
|
||
document.getElementById('exportBtn').addEventListener('click', () => {
|
||
this.showImportExportModal('export');
|
||
});
|
||
|
||
// 模态框关闭
|
||
document.querySelectorAll('.close-modal').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this.closeAllModals();
|
||
});
|
||
});
|
||
|
||
// 网址表单提交 - 使用bind确保正确的this上下文
|
||
document.getElementById('urlForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
this.handleUrlFormSubmit();
|
||
});
|
||
|
||
// 分类表单提交
|
||
document.getElementById('categoryForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
this.handleCategoryFormSubmit();
|
||
});
|
||
|
||
// 标签输入
|
||
document.getElementById('tagsInput').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
this.addTagToInput(e.target.value);
|
||
e.target.value = '';
|
||
}
|
||
});
|
||
|
||
// 文件导入
|
||
document.getElementById('jsonFileInput').addEventListener('change', (e) => {
|
||
this.handleFileImport(e.target.files[0]);
|
||
});
|
||
|
||
// 添加调试日志
|
||
console.log('事件监听器已设置完成');
|
||
|
||
// 事件委托
|
||
document.addEventListener('click', (e) => {
|
||
// 分类点击
|
||
if (e.target.closest('.category-item')) {
|
||
const categoryItem = e.target.closest('.category-item');
|
||
const categoryId = categoryItem.dataset.id;
|
||
this.setCurrentCategory(categoryId);
|
||
}
|
||
|
||
// 标签点击
|
||
if (e.target.closest('.tag')) {
|
||
const tag = e.target.closest('.tag').dataset.tag;
|
||
this.setCurrentTag(tag);
|
||
}
|
||
|
||
// 网址卡片操作
|
||
if (e.target.closest('.url-card')) {
|
||
const urlCard = e.target.closest('.url-card');
|
||
const urlId = urlCard.dataset.id;
|
||
|
||
// 复选框选择
|
||
if (e.target.type === 'checkbox') {
|
||
this.toggleUrlSelection(urlId, e.target.checked);
|
||
}
|
||
|
||
// 收藏按钮
|
||
if (e.target.closest('.favorite-btn')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.toggleFavorite(urlId);
|
||
}
|
||
|
||
// 编辑按钮
|
||
if (e.target.closest('.edit-url')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.editUrl(urlId);
|
||
}
|
||
|
||
// 删除按钮
|
||
if (e.target.closest('.delete-url')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
if (confirm('确定要删除这个网址吗?')) {
|
||
this.deleteUrl(urlId);
|
||
}
|
||
}
|
||
|
||
// 打开网址按钮
|
||
if (e.target.closest('.open-url')) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.openUrl(urlId);
|
||
}
|
||
}
|
||
|
||
// 导入/导出标签页切换
|
||
if (e.target.closest('.tab-btn')) {
|
||
const tabBtn = e.target.closest('.tab-btn');
|
||
const tab = tabBtn.dataset.tab;
|
||
this.switchImportExportTab(tab);
|
||
}
|
||
|
||
// 下载JSON按钮
|
||
if (e.target.id === 'downloadJsonBtn') {
|
||
this.downloadJson();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 模态框方法
|
||
showUrlModal(urlId = null) {
|
||
this.editingUrlId = urlId;
|
||
const modal = document.getElementById('urlModal');
|
||
const title = document.getElementById('modalTitle');
|
||
const form = document.getElementById('urlForm');
|
||
|
||
if (urlId) {
|
||
// 编辑模式
|
||
title.textContent = '编辑网址';
|
||
const url = this.urls.find(u => u.id === urlId);
|
||
if (url) {
|
||
document.getElementById('urlTitle').value = url.title;
|
||
document.getElementById('urlAddress').value = url.url;
|
||
document.getElementById('urlDescription').value = url.description || '';
|
||
document.getElementById('urlCategory').value = url.category || '';
|
||
document.getElementById('urlIcon').value = url.icon || 'fas fa-globe';
|
||
document.getElementById('urlFavorite').checked = url.favorite || false;
|
||
|
||
// 设置标签
|
||
const tagsDisplay = document.getElementById('tagsDisplay');
|
||
tagsDisplay.innerHTML = '';
|
||
if (url.tags) {
|
||
url.tags.forEach(tag => {
|
||
this.addTagToDisplay(tag);
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
// 添加模式
|
||
title.textContent = '添加网址';
|
||
form.reset();
|
||
document.getElementById('tagsDisplay').innerHTML = '';
|
||
}
|
||
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
showCategoryModal() {
|
||
const modal = document.getElementById('categoryModal');
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
showImportExportModal(tab = 'export') {
|
||
const modal = document.getElementById('importExportModal');
|
||
const title = document.getElementById('importExportTitle');
|
||
|
||
if (tab === 'export') {
|
||
title.textContent = '导出数据';
|
||
this.updateExportPreview();
|
||
} else {
|
||
title.textContent = '导入数据';
|
||
}
|
||
|
||
this.switchImportExportTab(tab);
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
closeAllModals() {
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
modal.classList.remove('active');
|
||
});
|
||
this.editingUrlId = null;
|
||
// 强制触发重排,确保弹窗关闭
|
||
void document.body.offsetHeight;
|
||
}
|
||
|
||
// 表单处理
|
||
handleUrlFormSubmit() {
|
||
const title = document.getElementById('urlTitle').value.trim();
|
||
const url = document.getElementById('urlAddress').value.trim();
|
||
const description = document.getElementById('urlDescription').value.trim();
|
||
const category = document.getElementById('urlCategory').value;
|
||
const icon = document.getElementById('urlIcon').value;
|
||
const favorite = document.getElementById('urlFavorite').checked;
|
||
|
||
// 获取标签
|
||
const tags = [];
|
||
document.querySelectorAll('#tagsDisplay .tag-input-tag').forEach(tagEl => {
|
||
const span = tagEl.querySelector('span');
|
||
if (span) {
|
||
tags.push(span.textContent);
|
||
}
|
||
});
|
||
|
||
if (!title || !url) {
|
||
alert('标题和网址是必填项');
|
||
return false;
|
||
}
|
||
|
||
const urlData = {
|
||
title,
|
||
url,
|
||
description,
|
||
category,
|
||
icon,
|
||
tags,
|
||
favorite
|
||
};
|
||
|
||
// 保存数据(只对核心保存操作进行错误处理)
|
||
try {
|
||
if (this.editingUrlId) {
|
||
const success = this.updateUrl(this.editingUrlId, urlData);
|
||
if (!success) {
|
||
alert('更新失败:未找到要更新的网址');
|
||
return false;
|
||
}
|
||
} else {
|
||
this.addUrl(urlData);
|
||
}
|
||
// 如果执行到这里,说明保存成功(addUrl/updateUrl 没有抛出异常)
|
||
// render() 已经在 addUrl/updateUrl 中调用,页面应该已经刷新
|
||
} catch (error) {
|
||
console.error('保存网址时出错:', error);
|
||
// 只有在真正的保存操作失败时才提示
|
||
alert('保存失败,请重试');
|
||
return false;
|
||
}
|
||
|
||
// 重置表单(这些操作不应该影响保存结果)
|
||
try {
|
||
const form = document.getElementById('urlForm');
|
||
if (form) {
|
||
form.reset();
|
||
}
|
||
const tagsDisplay = document.getElementById('tagsDisplay');
|
||
if (tagsDisplay) {
|
||
tagsDisplay.innerHTML = '';
|
||
}
|
||
} catch (error) {
|
||
console.warn('重置表单时出错:', error);
|
||
// 不影响保存结果,继续执行
|
||
}
|
||
|
||
// 关闭弹窗(addUrl/updateUrl 已经调用了 render() 刷新页面)
|
||
this.closeAllModals();
|
||
|
||
return false;
|
||
}
|
||
|
||
handleCategoryFormSubmit() {
|
||
const name = document.getElementById('categoryName').value.trim();
|
||
const color = document.getElementById('categoryColor').value;
|
||
const icon = document.getElementById('categoryIcon').value;
|
||
|
||
if (!name) {
|
||
alert('分类名称是必填项');
|
||
return;
|
||
}
|
||
|
||
const categoryData = {
|
||
name,
|
||
color,
|
||
icon
|
||
};
|
||
|
||
this.addCategory(categoryData);
|
||
this.closeAllModals();
|
||
}
|
||
|
||
// 标签处理
|
||
addTagToInput(tagText) {
|
||
if (!tagText.trim()) return;
|
||
|
||
const tagsDisplay = document.getElementById('tagsDisplay');
|
||
const existingTags = Array.from(tagsDisplay.querySelectorAll('.tag-input-tag span'))
|
||
.map(span => span.textContent);
|
||
|
||
if (!existingTags.includes(tagText.trim())) {
|
||
this.addTagToDisplay(tagText.trim());
|
||
}
|
||
}
|
||
|
||
addTagToDisplay(tagText) {
|
||
const tagsDisplay = document.getElementById('tagsDisplay');
|
||
const tagEl = document.createElement('div');
|
||
tagEl.className = 'tag-input-tag';
|
||
tagEl.innerHTML = `
|
||
<span>${tagText}</span>
|
||
<button type="button">×</button>
|
||
`;
|
||
|
||
tagEl.querySelector('button').addEventListener('click', () => {
|
||
tagEl.remove();
|
||
});
|
||
|
||
tagsDisplay.appendChild(tagEl);
|
||
}
|
||
|
||
// 文件导入/导出
|
||
handleFileImport(file) {
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
try {
|
||
const data = JSON.parse(e.target.result);
|
||
|
||
if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) {
|
||
if (data.urls) this.urls = data.urls;
|
||
if (data.categories) this.categories = data.categories;
|
||
this.saveData();
|
||
this.render();
|
||
this.closeAllModals();
|
||
alert('数据导入成功!');
|
||
}
|
||
} catch (error) {
|
||
alert('文件格式错误,请选择有效的JSON文件');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
updateExportPreview() {
|
||
const preview = document.getElementById('exportDataPreview');
|
||
const data = {
|
||
urls: this.urls,
|
||
categories: this.categories,
|
||
exportedAt: new Date().toISOString()
|
||
};
|
||
preview.textContent = JSON.stringify(data, null, 2);
|
||
}
|
||
|
||
downloadJson() {
|
||
const data = {
|
||
urls: this.urls,
|
||
categories: this.categories,
|
||
exportedAt: new Date().toISOString()
|
||
};
|
||
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `url-manager-backup-${new Date().toISOString().split('T')[0]}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
switchImportExportTab(tab) {
|
||
// 更新按钮状态
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||
});
|
||
|
||
// 更新内容显示
|
||
document.querySelectorAll('.tab-content').forEach(content => {
|
||
content.classList.toggle('active', content.id === `${tab}Tab`);
|
||
});
|
||
}
|
||
|
||
// 其他方法
|
||
setCurrentCategory(categoryId) {
|
||
this.currentCategory = categoryId;
|
||
this.render();
|
||
|
||
// 更新UI状态
|
||
document.querySelectorAll('.category-item').forEach(item => {
|
||
item.classList.toggle('active', item.dataset.id === categoryId);
|
||
});
|
||
}
|
||
|
||
setCurrentTag(tag) {
|
||
if (this.currentTag === tag) {
|
||
this.currentTag = ''; // 取消选择
|
||
} else {
|
||
this.currentTag = tag;
|
||
}
|
||
this.renderUrls();
|
||
this.renderTags();
|
||
}
|
||
|
||
setViewMode(mode) {
|
||
this.viewMode = mode;
|
||
const urlsContainer = document.getElementById('urlsContainer');
|
||
const gridBtn = document.getElementById('gridViewBtn');
|
||
const listBtn = document.getElementById('listViewBtn');
|
||
|
||
urlsContainer.classList.remove('grid-view', 'list-view');
|
||
urlsContainer.classList.add(`${mode}-view`);
|
||
|
||
gridBtn.classList.toggle('active', mode === 'grid');
|
||
listBtn.classList.toggle('active', mode === 'list');
|
||
}
|
||
|
||
toggleUrlSelection(urlId, checked) {
|
||
if (checked) {
|
||
this.selectedUrls.add(urlId);
|
||
} else {
|
||
this.selectedUrls.delete(urlId);
|
||
}
|
||
|
||
this.updateBulkActions();
|
||
this.renderUrls();
|
||
}
|
||
|
||
updateBulkActions() {
|
||
const selectedCount = document.getElementById('selectedCount');
|
||
const bulkPanel = document.getElementById('bulkActionsPanel');
|
||
|
||
selectedCount.textContent = `已选择 ${this.selectedUrls.size} 个项目`;
|
||
|
||
if (this.selectedUrls.size > 0) {
|
||
bulkPanel.classList.add('active');
|
||
} else {
|
||
bulkPanel.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
showBulkActions() {
|
||
// 显示批量操作面板
|
||
const bulkPanel = document.getElementById('bulkActionsPanel');
|
||
bulkPanel.classList.add('active');
|
||
}
|
||
|
||
hideBulkActions() {
|
||
// 清除选择并隐藏面板
|
||
this.selectedUrls.clear();
|
||
const bulkPanel = document.getElementById('bulkActionsPanel');
|
||
bulkPanel.classList.remove('active');
|
||
this.renderUrls();
|
||
}
|
||
|
||
openUrl(urlId) {
|
||
const url = this.urls.find(u => u.id === urlId);
|
||
if (url) {
|
||
window.open(url.url, '_blank');
|
||
|
||
// 更新访问记录
|
||
url.lastAccessed = new Date().toISOString();
|
||
url.accessCount = (url.accessCount || 0) + 1;
|
||
this.saveData();
|
||
}
|
||
}
|
||
|
||
openSelectedUrls() {
|
||
this.urls.forEach(url => {
|
||
if (this.selectedUrls.has(url.id)) {
|
||
window.open(url.url, '_blank');
|
||
|
||
// 更新访问记录
|
||
url.lastAccessed = new Date().toISOString();
|
||
url.accessCount = (url.accessCount || 0) + 1;
|
||
}
|
||
});
|
||
this.saveData();
|
||
}
|
||
|
||
editUrl(urlId) {
|
||
this.showUrlModal(urlId);
|
||
}
|
||
|
||
toggleTheme() {
|
||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||
const themeIcon = document.querySelector('#themeToggle i');
|
||
|
||
document.documentElement.setAttribute('data-theme', newTheme);
|
||
themeIcon.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||
|
||
localStorage.setItem('urlManager_theme', newTheme);
|
||
}
|
||
|
||
setupKeyboardShortcuts() {
|
||
document.addEventListener('keydown', (e) => {
|
||
// Ctrl+F 聚焦搜索框
|
||
if (e.ctrlKey && e.key === 'f') {
|
||
e.preventDefault();
|
||
document.getElementById('searchInput').focus();
|
||
}
|
||
|
||
// Ctrl+N 添加新网址
|
||
if (e.ctrlKey && e.key === 'n') {
|
||
e.preventDefault();
|
||
this.showUrlModal();
|
||
}
|
||
|
||
// Esc 关闭模态框或取消批量选择
|
||
if (e.key === 'Escape') {
|
||
if (this.selectedUrls.size > 0) {
|
||
this.hideBulkActions();
|
||
} else {
|
||
this.closeAllModals();
|
||
}
|
||
}
|
||
|
||
// Ctrl+S 保存数据(导出)
|
||
if (e.ctrlKey && e.key === 's') {
|
||
e.preventDefault();
|
||
this.showImportExportModal('export');
|
||
}
|
||
});
|
||
|
||
// 加载保存的主题
|
||
const savedTheme = localStorage.getItem('urlManager_theme') || 'light';
|
||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||
const themeIcon = document.querySelector('#themeToggle i');
|
||
themeIcon.className = savedTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
||
}
|
||
}
|
||
|
||
// 初始化应用
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
window.urlManager = new UrlManager();
|
||
});
|