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 @@
-
-
-
+
-
+
-
-
+
+
×
-
管理分类
-
-
-
-
-
-
-
+
管理员登录
+
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