diff --git a/frontend/index.html b/frontend/index.html index a4b1d8e..d14cea9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,24 +8,6 @@ - -
@@ -35,25 +17,23 @@
-
- - - - - - - - - -
+
+ + + + + + + + +
-
加载中...
- + - - diff --git a/frontend/script.js b/frontend/script.js index b13a4fe..666cdee 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -2,73 +2,221 @@ document.addEventListener('DOMContentLoaded', function() { // DOM元素 const categoriesContainer = document.getElementById('categories'); const addSiteModal = document.getElementById('addSiteModal'); - const manageCategoriesModal = document.getElementById('manageCategoriesModal'); + const loginModal = document.getElementById('loginModal'); const addSiteBtn = document.getElementById('addSiteBtn'); - const manageCategoriesBtn = document.getElementById('manageCategoriesBtn'); - const closeButtons = document.querySelectorAll('.close'); - const addSiteForm = document.getElementById('addSiteForm'); + const loginBtn = document.getElementById('loginBtn'); + const logoutBtn = document.getElementById('logoutBtn'); const exportBtn = document.getElementById('exportBtn'); const importBtn = document.getElementById('importBtn'); - const importFile = document.getElementById('importFile'); + const closeButtons = document.querySelectorAll('.close'); + const addSiteForm = document.getElementById('addSiteForm'); + const loginForm = document.getElementById('loginForm'); const searchInput = document.getElementById('searchInput'); const themeBtn = document.getElementById('themeBtn'); - const categoryList = document.getElementById('categoryList'); - const newCategoryName = document.getElementById('newCategoryName'); - const addCategoryBtn = document.getElementById('addCategoryBtn'); + const importFile = document.getElementById('importFile'); - // 当前主题 - const currentTheme = localStorage.getItem('theme') || 'light'; - document.documentElement.setAttribute('data-theme', currentTheme); - themeBtn.innerHTML = currentTheme === 'light' ? '' : ''; - - // 加载数据 + // 状态变量 + let authToken = localStorage.getItem('authToken'); + let isAdmin = false; + let currentTheme = localStorage.getItem('theme') || 'light'; + + // 初始化 + initTheme(); + checkAuthStatus(); + setupEventListeners(); loadSites(); - // 打开添加网站模态框 - addSiteBtn.addEventListener('click', () => { - addSiteModal.style.display = 'block'; - }); + // 初始化主题 + function initTheme() { + document.documentElement.setAttribute('data-theme', currentTheme); + themeBtn.innerHTML = currentTheme === 'light' ? '' : ''; + } - // 打开管理分类模态框 - manageCategoriesBtn.addEventListener('click', () => { - renderCategoryList(); - manageCategoriesModal.style.display = 'block'; - }); - - // 关闭模态框 - closeButtons.forEach(btn => { - btn.addEventListener('click', function() { - this.closest('.modal').style.display = 'none'; - }); - }); - - // 点击模态框外部关闭 - window.addEventListener('click', (e) => { - if (e.target.classList.contains('modal')) { - e.target.style.display = 'none'; + // 检查认证状态 + async function checkAuthStatus() { + if (authToken) { + try { + const response = await fetchWithAuth('/api/validate'); + if (response.ok) { + isAdmin = true; + setupAdminFeatures(); + } + } catch (error) { + console.error('Auth check failed:', error); + logout(); + } } - }); + updateAuthUI(); + } - // 添加网站表单提交 - addSiteForm.addEventListener('submit', async (e) => { + // 设置管理员功能 + function setupAdminFeatures() { + // 显示管理按钮 + addSiteBtn.style.display = 'flex'; + exportBtn.style.display = 'flex'; + importBtn.style.display = 'flex'; + + // 设置拖拽排序 + setupDragAndDrop(); + } + + // 更新认证UI + function updateAuthUI() { + if (isAdmin) { + loginBtn.style.display = 'none'; + logoutBtn.style.display = 'flex'; + } else { + loginBtn.style.display = 'flex'; + logoutBtn.style.display = 'none'; + } + } + + // 设置拖拽排序 + function setupDragAndDrop() { + let draggedItem = null; + + // 拖拽开始 + const handleDragStart = (e) => { + draggedItem = e.target; + e.target.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/html', e.target.innerHTML); + }; + + // 拖拽结束 + const handleDragEnd = () => { + draggedItem.classList.remove('dragging'); + draggedItem = null; + }; + + // 拖拽经过 + const handleDragOver = (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const afterElement = getDragAfterElement(categoriesContainer, e.clientY); + if (afterElement == null) { + categoriesContainer.appendChild(draggedItem); + } else { + categoriesContainer.insertBefore(draggedItem, afterElement); + } + }; + + // 获取拖拽后的元素 + const getDragAfterElement = (container, y) => { + const draggableElements = [...container.querySelectorAll('.site:not(.dragging)')]; + + return draggableElements.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; + }; + + // 为所有网站添加拖拽事件 + document.querySelectorAll('.site').forEach(site => { + site.draggable = isAdmin; + site.addEventListener('dragstart', handleDragStart); + site.addEventListener('dragend', handleDragEnd); + }); + + categoriesContainer.addEventListener('dragover', handleDragOver); + categoriesContainer.addEventListener('dragend', async () => { + // 更新排序 + const sites = []; + document.querySelectorAll('.site').forEach((siteEl, index) => { + sites.push({ + id: parseInt(siteEl.dataset.id), + order: index + }); + }); + + try { + const response = await fetchWithAuth('/api/sites/order', { + method: 'POST', + body: JSON.stringify(sites) + }); + + if (!response.ok) throw new Error('Failed to update order'); + } catch (error) { + console.error('Error updating order:', error); + alert('排序更新失败'); + } + }); + } + + // 设置事件监听器 + function setupEventListeners() { + // 主题切换 + themeBtn.addEventListener('click', toggleTheme); + + // 打开添加网站模态框 + addSiteBtn.addEventListener('click', () => addSiteModal.style.display = 'block'); + + // 打开登录模态框 + loginBtn.addEventListener('click', () => loginModal.style.display = 'block'); + + // 注销 + logoutBtn.addEventListener('click', logout); + + // 关闭模态框 + closeButtons.forEach(btn => { + btn.addEventListener('click', function() { + this.closest('.modal').style.display = 'none'; + }); + }); + + // 点击模态框外部关闭 + window.addEventListener('click', (e) => { + if (e.target.classList.contains('modal')) { + e.target.style.display = 'none'; + } + }); + + // 添加网站表单提交 + addSiteForm.addEventListener('submit', handleAddSite); + + // 登录表单提交 + loginForm.addEventListener('submit', handleLogin); + + // 搜索功能 + searchInput.addEventListener('input', handleSearch); + + // 导出数据 + exportBtn.addEventListener('click', handleExport); + + // 导入数据 + importBtn.addEventListener('click', () => importFile.click()); + importFile.addEventListener('change', handleImport); + } + + // 添加网站处理 + async function handleAddSite(e) { e.preventDefault(); const name = document.getElementById('siteName').value.trim(); - const url = document.getElementById('siteUrl').value.trim(); + let url = document.getElementById('siteUrl').value.trim(); const category = document.getElementById('siteCategory').value.trim(); const icon = document.getElementById('siteIcon').value.trim(); + // 确保URL有协议 + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + try { - const response = await fetch('/api/add', { + const response = await fetchWithAuth('/api/sites/add', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, body: JSON.stringify({ name, url, icon: icon || null, - category + category, + order: 0 }) }); @@ -84,12 +232,68 @@ document.addEventListener('DOMContentLoaded', function() { console.error('Error:', error); alert('添加网站时出错: ' + error.message); } - }); + } - // 导出数据 - exportBtn.addEventListener('click', async () => { + // 登录处理 + async function handleLogin(e) { + e.preventDefault(); + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value.trim(); + try { - const response = await fetch('/api/sites'); + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }) + }); + + if (!response.ok) throw new Error('登录失败'); + + const data = await response.json(); + authToken = data.token; + localStorage.setItem('authToken', authToken); + isAdmin = true; + + loginModal.style.display = 'none'; + loginForm.reset(); + setupAdminFeatures(); + updateAuthUI(); + loadSites(); + } catch (error) { + console.error('Login error:', error); + alert('登录失败: ' + error.message); + } + } + + // 注销处理 + function logout() { + localStorage.removeItem('authToken'); + authToken = null; + isAdmin = false; + updateAuthUI(); + window.location.reload(); + } + + // 搜索处理 + function handleSearch(e) { + const searchTerm = e.target.value.toLowerCase(); + document.querySelectorAll('.site').forEach(site => { + const name = site.getAttribute('data-name') || ''; + const url = site.getAttribute('data-url') || ''; + if (name.includes(searchTerm) || url.includes(searchTerm)) { + site.style.display = 'flex'; + } else { + site.style.display = 'none'; + } + }); + } + + // 导出处理 + async function handleExport() { + try { + const response = await fetchWithAuth('/api/export'); if (!response.ok) throw new Error('导出失败'); const data = await response.json(); @@ -106,14 +310,10 @@ document.addEventListener('DOMContentLoaded', function() { console.error('导出失败:', error); alert('导出数据时出错'); } - }); + } - // 导入数据 - importBtn.addEventListener('click', () => { - importFile.click(); - }); - - importFile.addEventListener('change', async (e) => { + // 导入处理 + async function handleImport(e) { const file = e.target.files[0]; if (!file) return; @@ -123,11 +323,8 @@ document.addEventListener('DOMContentLoaded', function() { const importedData = JSON.parse(e.target.result); if (confirm('确定要导入数据吗?这将覆盖当前所有导航数据。')) { - const response = await fetch('/api/import', { + const response = await fetchWithAuth('/api/import', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, body: JSON.stringify(importedData) }); @@ -145,57 +342,39 @@ document.addEventListener('DOMContentLoaded', function() { // 清空input,以便可以重复导入同一文件 e.target.value = ''; - }); + } - // 添加新分类 - addCategoryBtn.addEventListener('click', async () => { - const categoryName = newCategoryName.value.trim(); - if (!categoryName) return; + // 切换主题 + function toggleTheme() { + currentTheme = currentTheme === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', currentTheme); + localStorage.setItem('theme', currentTheme); + themeBtn.innerHTML = currentTheme === 'light' ? '' : ''; + } + + // 带认证的fetch + async function fetchWithAuth(url, options = {}) { + const headers = { + 'Content-Type': 'application/json', + ...options.headers + }; - try { - // 这里需要根据你的Go后端API进行调整 - const response = await fetch('/api/categories', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name: categoryName }) - }); - - if (!response.ok) throw new Error('添加分类失败'); - - newCategoryName.value = ''; - renderCategoryList(); - loadSites(); - } catch (error) { - console.error('添加分类失败:', error); - alert('添加分类时出错: ' + error.message); + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; } - }); - - // 搜索功能 - searchInput.addEventListener('input', (e) => { - const searchTerm = e.target.value.toLowerCase(); - document.querySelectorAll('.site').forEach(site => { - const name = site.getAttribute('data-name') || ''; - const url = site.getAttribute('data-url') || ''; - if (name.includes(searchTerm) || url.includes(searchTerm)) { - site.style.display = 'flex'; - } else { - site.style.display = 'none'; - } + + return fetch(url, { + ...options, + headers }); - }); - - // 主题切换 - themeBtn.addEventListener('click', toggleTheme); + } // 加载网站数据 async function loadSites() { categoriesContainer.innerHTML = '
加载中...
'; try { - const response = await fetch('/api/sites'); + const response = await fetchWithAuth('/api/sites'); if (!response.ok) throw new Error('Network response was not ok'); const sites = await response.json(); @@ -241,8 +420,10 @@ document.addEventListener('DOMContentLoaded', function() { siteElement.className = 'site'; siteElement.href = site.url; siteElement.target = '_blank'; - siteElement.setAttribute('data-name', site.name.toLowerCase()); - siteElement.setAttribute('data-url', site.url.toLowerCase()); + siteElement.dataset.id = site.id; + siteElement.dataset.name = site.name.toLowerCase(); + siteElement.dataset.url = site.url.toLowerCase(); + siteElement.draggable = isAdmin; const iconElement = document.createElement('div'); iconElement.className = 'site-icon'; @@ -260,37 +441,37 @@ document.addEventListener('DOMContentLoaded', function() { nameElement.className = 'site-name'; nameElement.textContent = site.name; - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'delete-btn'; - deleteBtn.innerHTML = '×'; - deleteBtn.addEventListener('click', async (e) => { - e.preventDefault(); - e.stopPropagation(); - if (confirm(`确定要删除 "${site.name}" 吗?`)) { - try { - const response = await fetch(`/api/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: site.id }) - }); - - if (response.ok) { - loadSites(); - } else { - throw new Error('删除失败'); + // 管理员功能:删除按钮 + if (isAdmin) { + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'delete-btn'; + deleteBtn.innerHTML = '×'; + deleteBtn.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + if (confirm(`确定要删除 "${site.name}" 吗?`)) { + try { + const response = await fetchWithAuth('/api/sites/delete', { + method: 'POST', + body: JSON.stringify({ id: site.id }) + }); + + if (response.ok) { + loadSites(); + } else { + throw new Error('删除失败'); + } + } catch (error) { + console.error('Error deleting site:', error); + alert('删除网站时出错'); } - } catch (error) { - console.error('Error deleting site:', error); - alert('删除网站时出错'); } - } - }); + }); + siteElement.appendChild(deleteBtn); + } siteElement.appendChild(iconElement); siteElement.appendChild(nameElement); - siteElement.appendChild(deleteBtn); sitesContainer.appendChild(siteElement); }); @@ -298,70 +479,10 @@ document.addEventListener('DOMContentLoaded', function() { categoryElement.appendChild(sitesContainer); categoriesContainer.appendChild(categoryElement); } - } - - // 渲染分类列表 - async function renderCategoryList() { - categoryList.innerHTML = '
加载中...
'; - try { - const response = await fetch('/api/sites'); - if (!response.ok) throw new Error('Network response was not ok'); - - const sites = await response.json(); - - // 获取所有分类 - const categories = new Set(); - sites.forEach(site => categories.add(site.category)); - - categoryList.innerHTML = ''; - - categories.forEach(category => { - const categoryItem = document.createElement('div'); - categoryItem.className = 'category-item'; - - const categoryName = document.createElement('span'); - categoryName.textContent = category; - - const deleteBtn = document.createElement('button'); - deleteBtn.innerHTML = ' 删除'; - deleteBtn.addEventListener('click', async () => { - if (confirm(`确定要删除分类 "${category}" 吗?这将删除该分类下的所有网站。`)) { - try { - // 这里需要根据你的Go后端API进行调整 - const response = await fetch(`/api/categories/${encodeURIComponent(category)}`, { - method: 'DELETE' - }); - - if (response.ok) { - renderCategoryList(); - loadSites(); - } else { - throw new Error('删除分类失败'); - } - } catch (error) { - console.error('删除分类失败:', error); - alert('删除分类时出错: ' + error.message); - } - } - }); - - categoryItem.appendChild(categoryName); - categoryItem.appendChild(deleteBtn); - categoryList.appendChild(categoryItem); - }); - } catch (error) { - console.error('Error loading categories:', error); - categoryList.innerHTML = '
加载分类失败
'; + // 如果是管理员,设置拖拽功能 + if (isAdmin) { + setupDragAndDrop(); } } - - // 切换主题 - function toggleTheme() { - const currentTheme = document.documentElement.getAttribute('data-theme'); - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - document.documentElement.setAttribute('data-theme', newTheme); - localStorage.setItem('theme', newTheme); - themeBtn.innerHTML = newTheme === 'light' ? '' : ''; - } }); \ No newline at end of file diff --git a/frontend/styles.css b/frontend/styles.css index 03989a4..fb773e6 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -2,6 +2,7 @@ --primary-color: #3498db; --secondary-color: #2ecc71; --danger-color: #e74c3c; + --warning-color: #f39c12; --text-color: #333; --bg-color: #f5f5f5; --card-color: #fff; @@ -13,6 +14,7 @@ --primary-color: #2980b9; --secondary-color: #27ae60; --danger-color: #c0392b; + --warning-color: #d35400; --text-color: #f5f5f5; --bg-color: #121212; --card-color: #1e1e1e; @@ -53,12 +55,16 @@ header { .header-left, .header-right { display: flex; align-items: center; - gap: 20px; + gap: 15px; + flex-wrap: wrap; } h1 { font-size: 2rem; color: var(--primary-color); + display: flex; + align-items: center; + gap: 10px; } .search-box { @@ -84,13 +90,7 @@ h1 { opacity: 0.7; } -.actions { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.actions button { +button { padding: 8px 16px; background-color: var(--primary-color); color: white; @@ -100,23 +100,33 @@ h1 { display: flex; align-items: center; gap: 5px; -} - -.actions button:hover { - opacity: 0.9; -} - -.actions button i { font-size: 0.9rem; } -.theme-switcher button { - background: none; - border: none; - color: var(--text-color); - font-size: 1.2rem; - cursor: pointer; - padding: 5px; +button:hover { + opacity: 0.9; +} + +button i { + font-size: 0.9rem; +} + +#loginBtn, #logoutBtn { + background-color: var(--secondary-color); +} + +#logoutBtn { + background-color: var(--warning-color); +} + +.loading, .error { + text-align: center; + padding: 20px; + color: var(--primary-color); +} + +.error { + color: var(--danger-color); } .categories { @@ -154,7 +164,7 @@ h1 { align-items: center; text-decoration: none; color: var(--text-color); - transition: transform 0.2s; + transition: all 0.2s; position: relative; padding: 10px; border-radius: 8px; @@ -180,6 +190,13 @@ h1 { color: var(--primary-color); } +.site-icon img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + .site-name { text-align: center; font-size: 0.9rem; @@ -200,12 +217,26 @@ h1 { font-size: 12px; cursor: pointer; display: none; + align-items: center; + justify-content: center; } .site:hover .delete-btn { display: flex; - align-items: center; - justify-content: center; +} + +/* 拖拽相关样式 */ +.site[draggable="true"] { + cursor: grab; +} + +.site[draggable="true"]:active { + cursor: grabbing; +} + +.site.dragging { + opacity: 0.5; + background-color: var(--primary-color); } /* 模态框样式 */ @@ -269,8 +300,7 @@ h1 { gap: 8px; } -.form-group input, -.form-group select { +.form-group input { width: 100%; padding: 10px; border: 1px solid var(--border-color); @@ -298,73 +328,6 @@ form button:hover { opacity: 0.9; } -/* 分类管理样式 */ -.category-list { - margin-bottom: 20px; - max-height: 300px; - overflow-y: auto; -} - -.category-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; - margin-bottom: 10px; - background-color: var(--bg-color); - border-radius: 4px; -} - -.category-item span { - flex-grow: 1; -} - -.category-item button { - background-color: var(--danger-color); - color: white; - border: none; - border-radius: 4px; - padding: 5px 10px; - cursor: pointer; -} - -.add-category { - display: flex; - gap: 10px; -} - -.add-category input { - flex-grow: 1; - padding: 10px; - border: 1px solid var(--border-color); - border-radius: 4px; - background-color: var(--bg-color); - color: var(--text-color); -} - -.add-category button { - background-color: var(--secondary-color); - color: white; - border: none; - padding: 10px 15px; - border-radius: 4px; - cursor: pointer; - display: flex; - align-items: center; - gap: 5px; -} - -/* 拖拽样式 */ -.sortable-ghost { - opacity: 0.5; - background: var(--primary-color); -} - -.sortable-chosen { - box-shadow: 0 0 10px var(--shadow-color); -} - -/* 响应式设计 */ @media (max-width: 768px) { header { flex-direction: column; @@ -382,12 +345,8 @@ form button:hover { width: 100%; } - .actions { - justify-content: space-between; - } - - .actions button { - flex-grow: 1; + .header-right button { + width: 100%; justify-content: center; } @@ -395,26 +354,7 @@ form button:hover { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); } - .add-category { - flex-direction: column; + .modal-content { + margin: 20% auto; } -} - -/* 添加拖拽相关样式 */ -.site[draggable="true"] { - cursor: grab; -} - -.site[draggable="true"]:active { - cursor: grabbing; -} - -.site.dragging { - opacity: 0.5; - background-color: var(--primary-color); -} - -/* 登录按钮样式 */ -#loginBtn, #logoutBtn { - background-color: var(--secondary-color); } \ No newline at end of file