Files
navigation-app/frontend/script.js
2025-03-26 02:48:52 +08:00

488 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener('DOMContentLoaded', function() {
// DOM元素
const categoriesContainer = document.getElementById('categories');
const addSiteModal = document.getElementById('addSiteModal');
const loginModal = document.getElementById('loginModal');
const addSiteBtn = document.getElementById('addSiteBtn');
const loginBtn = document.getElementById('loginBtn');
const logoutBtn = document.getElementById('logoutBtn');
const exportBtn = document.getElementById('exportBtn');
const importBtn = document.getElementById('importBtn');
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 importFile = document.getElementById('importFile');
// 状态变量
let authToken = localStorage.getItem('authToken');
let isAdmin = false;
let currentTheme = localStorage.getItem('theme') || 'light';
// 初始化
initTheme();
checkAuthStatus();
setupEventListeners();
loadSites();
// 初始化主题
function initTheme() {
document.documentElement.setAttribute('data-theme', currentTheme);
themeBtn.innerHTML = currentTheme === 'light' ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
}
// 检查认证状态
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();
}
// 设置管理员功能
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();
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 fetchWithAuth('/api/sites/add', {
method: 'POST',
body: JSON.stringify({
name,
url,
icon: icon || null,
category,
order: 0
})
});
if (!response.ok) throw new Error('添加失败');
// 清空表单并关闭模态框
addSiteForm.reset();
addSiteModal.style.display = 'none';
// 重新加载数据
loadSites();
} catch (error) {
console.error('Error:', error);
alert('添加网站时出错: ' + error.message);
}
}
// 登录处理
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/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();
const dataStr = JSON.stringify(data, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileDefaultName = `导航数据_${new Date().toISOString().slice(0,10)}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
} catch (error) {
console.error('导出失败:', error);
alert('导出数据时出错');
}
}
// 导入处理
async function handleImport(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const importedData = JSON.parse(e.target.result);
if (confirm('确定要导入数据吗?这将覆盖当前所有导航数据。')) {
const response = await fetchWithAuth('/api/import', {
method: 'POST',
body: JSON.stringify(importedData)
});
if (!response.ok) throw new Error('导入失败');
alert('数据导入成功!');
loadSites();
}
} catch (error) {
console.error('导入失败:', error);
alert('导入数据时出错: ' + error.message);
}
};
reader.readAsText(file);
// 清空input以便可以重复导入同一文件
e.target.value = '';
}
// 切换主题
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
themeBtn.innerHTML = currentTheme === 'light' ? '<i class="fas fa-moon"></i>' : '<i class="fas fa-sun"></i>';
}
// 带认证的fetch
async function fetchWithAuth(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
return fetch(url, {
...options,
headers
});
}
// 加载网站数据
async function loadSites() {
categoriesContainer.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await fetchWithAuth('/api/sites');
if (!response.ok) throw new Error('Network response was not ok');
const sites = await response.json();
renderSites(sites);
} catch (error) {
console.error('Error loading sites:', error);
categoriesContainer.innerHTML = '<div class="error">加载失败,请刷新重试</div>';
}
}
// 渲染网站
function renderSites(sites) {
if (!sites || sites.length === 0) {
categoriesContainer.innerHTML = '<p>暂无网站,请添加您的第一个网站。</p>';
return;
}
// 按分类分组
const categories = {};
sites.forEach(site => {
if (!categories[site.category]) {
categories[site.category] = [];
}
categories[site.category].push(site);
});
// 清空容器
categoriesContainer.innerHTML = '';
// 渲染每个分类
for (const category in categories) {
const categoryElement = document.createElement('div');
categoryElement.className = 'category';
const categoryTitle = document.createElement('h2');
categoryTitle.innerHTML = `<i class="fas fa-folder"></i> ${category}`;
const sitesContainer = document.createElement('div');
sitesContainer.className = 'sites';
categories[category].forEach(site => {
const siteElement = document.createElement('a');
siteElement.className = 'site';
siteElement.href = site.url;
siteElement.target = '_blank';
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';
if (site.icon) {
const img = document.createElement('img');
img.src = site.icon;
img.alt = site.name;
iconElement.appendChild(img);
} else {
iconElement.textContent = site.name.charAt(0).toUpperCase();
}
const nameElement = document.createElement('span');
nameElement.className = 'site-name';
nameElement.textContent = site.name;
// 管理员功能:删除按钮
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('删除网站时出错');
}
}
});
siteElement.appendChild(deleteBtn);
}
siteElement.appendChild(iconElement);
siteElement.appendChild(nameElement);
sitesContainer.appendChild(siteElement);
});
categoryElement.appendChild(categoryTitle);
categoryElement.appendChild(sitesContainer);
categoriesContainer.appendChild(categoryElement);
}
// 如果是管理员,设置拖拽功能
if (isAdmin) {
setupDragAndDrop();
}
}
});