Files
CloudflareWorkerNav/worker.js
Lucas dce0c0acf1 更新 worker.js
增加一键导出/导入功能
2026-02-24 13:37:07 +08:00

2192 lines
81 KiB
JavaScript
Raw 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.
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const { pathname } = url;
try {
// 登录请求处理
if (pathname === '/login' && request.method === 'POST') {
const formData = new URLSearchParams(await request.text());
const username = formData.get('username');
const password = formData.get('password');
if (username === envusername && password === envpassword) {
return new Response(await renderNavigationPage(), {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
// 登录失败时直接返回登录页面并显示错误
return new Response(await renderLoginPage(true), {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
// 路由处理
switch (pathname) {
case '/':
case '/login.html':
return new Response(await renderLoginPage(), {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
case '/privacy':
return serveStaticFile('privacy.html');
case '/data':
return fetchNavigationData();
case '/add-category':
if (request.method === 'POST') return addCategory(request);
break;
case '/add-site':
if (request.method === 'POST') return addSite(request);
break;
case '/delete-category':
if (request.method === 'POST') return deleteCategory(request);
break;
case '/delete-site':
if (request.method === 'POST') return deleteSite(request);
break;
case '/edit-site':
if (request.method === 'POST') return editSite(request);
break;
case '/edit-category':
if (request.method === 'POST') return editCategory(request);
break;
// 在handleRequest中添加切换分类状态的路由
case '/toggle-category':
if (request.method === 'POST') return toggleCategory(request);
break;
// 在handleRequest的switch语句中添加新路由
case '/reorder-site':
if (request.method === 'POST') return reorderSite(request);
break;
case '/load-notification':
return loadNotification();
case '/export':
return exportData(); // 导出接口
case '/import':
if (request.method === 'POST') return importData(request);
break;
}
// 未匹配的路由返回404
return serveStaticFile('NotFound.html', 404);
} catch (error) {
console.error('Error handling request:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
// KV 数据操作函数
async function getNavigationData() {
const data = await NAVIGATION_DATA.get('data');
const parsedData = data ? JSON.parse(data) : { categories: [] };
// 初始化折叠状态
parsedData.categories.forEach(category => {
if (typeof category.collapsed === 'undefined') {
category.collapsed = false; // 默认展开状态
}
});
return parsedData;
}
// 导出导航数据为 JSON 文件
async function exportData() {
const navigationData = await getNavigationData(); // 复用已有函数
const jsonStr = JSON.stringify(navigationData, null, 2);
return new Response(jsonStr, {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': 'attachment; filename="navigation_backup.json"',
},
});
}
// 从上传的 JSON 文件导入导航数据
async function importData(request) {
try {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return new Response(JSON.stringify({ error: '请选择要导入的文件' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// 读取文件内容
const fileText = await file.text();
const importedData = JSON.parse(fileText);
// 简单验证格式:必须包含 categories 数组
if (!importedData || !Array.isArray(importedData.categories)) {
return new Response(JSON.stringify({ error: '无效的备份文件格式,缺少 categories 数组' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// 可选:进一步验证每个分类和站点结构(这里略,可根据需要补充)
// 写入 KV
await NAVIGATION_DATA.put('data', JSON.stringify(importedData));
return new Response(JSON.stringify({ message: '数据导入成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
} catch (error) {
return new Response(JSON.stringify({ error: '导入失败:' + error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
// 添加站点重新排序功能
async function reorderSite(request) {
const requestBody = await request.json();
const { categoryIndex, oldIndex, newIndex } = requestBody;
const navigationData = await getNavigationData();
// 验证分类索引
if (categoryIndex < 0 || categoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const category = navigationData.categories[categoryIndex];
// 验证站点索引
if (oldIndex < 0 || oldIndex >= category.sites.length ||
newIndex < 0 || newIndex >= category.sites.length) {
return new Response(JSON.stringify({ error: '无效站点索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 移动站点位置
const siteToMove = category.sites.splice(oldIndex, 1)[0];
category.sites.splice(newIndex, 0, siteToMove);
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '站点顺序已更新' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
// 添加编辑分类的API处理函数
async function editCategory(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 验证索引有效性
if (requestBody.categoryIndex < 0 ||
requestBody.categoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 检查新名称是否已存在
const newName = requestBody.newName.trim();
const categoryExists = navigationData.categories.some(
(cat, index) => index !== requestBody.categoryIndex && cat.name === newName
);
if (categoryExists) {
return new Response(JSON.stringify({ error: '分类名称已存在' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 更新分类名称
navigationData.categories[requestBody.categoryIndex].name = newName;
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '分类名称修改成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
// 添加切换分类折叠状态的API处理函数
async function toggleCategory(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 验证索引有效性
if (requestBody.categoryIndex < 0 ||
requestBody.categoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 切换折叠状态
const category = navigationData.categories[requestBody.categoryIndex];
category.collapsed = !category.collapsed;
await setNavigationData(navigationData);
return new Response(JSON.stringify({
message: '状态更新成功',
collapsed: category.collapsed
}), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
// 切换分类折叠状态
async function toggleCategoryState(categoryIndex) {
const response = await fetch('/toggle-category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ categoryIndex })
});
if (response.ok) {
const result = await response.json();
const categoryElement = document.getElementById(`category-sites-${categoryIndex}`);
const toggleIcon = document.querySelector(`.toggle-btn[onclick="toggleCategoryState(${categoryIndex})"] .iconify`);
if (result.collapsed) {
categoryElement.style.display = 'none';
toggleIcon.setAttribute('data-icon', 'mdi:chevron-right');
} else {
categoryElement.style.display = 'flex';
toggleIcon.setAttribute('data-icon', 'mdi:chevron-down');
}
}
}
async function setNavigationData(data) {
await NAVIGATION_DATA.put('data', JSON.stringify(data));
}
// 静态文件服务
async function serveStaticFile(filename, status = 200) {
const content = await STATIC_FILES.get(filename);
if (content) {
return new Response(content, {
status,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
return new Response(`${filename} not found`, { status: 404 });
}
// API 端点处理函数
async function fetchNavigationData() {
const navigationData = await getNavigationData();
return new Response(JSON.stringify(navigationData), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-cache'
}
});
}
async function addCategory(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 检查分类是否已存在
const categoryExists = navigationData.categories.some(
cat => cat.name === requestBody.name
);
if (categoryExists) {
return new Response(JSON.stringify({ error: '分类已存在' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
navigationData.categories.push({
name: requestBody.name,
sites: []
});
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '分类添加成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
async function addSite(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 验证索引有效性
if (requestBody.categoryIndex < 0 ||
requestBody.categoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 添加站点描述字段
navigationData.categories[requestBody.categoryIndex].sites.push({
name: requestBody.siteName,
url: requestBody.siteUrl,
icon: requestBody.siteIcon,
info: requestBody.siteInfo || '' // 添加站点描述
});
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '站点添加成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
async function editSite(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 获取原始分类索引和新分类索引
const originalCategoryIndex = requestBody.originalCategoryIndex;
const newCategoryIndex = requestBody.newCategoryIndex;
const siteIndex = requestBody.siteIndex;
// 验证索引有效性
if (originalCategoryIndex < 0 ||
originalCategoryIndex >= navigationData.categories.length ||
newCategoryIndex < 0 ||
newCategoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const originalCategory = navigationData.categories[originalCategoryIndex];
if (siteIndex < 0 || siteIndex >= originalCategory.sites.length) {
return new Response(JSON.stringify({ error: '无效站点索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 获取站点对象
const site = originalCategory.sites[siteIndex];
// 更新站点信息
site.name = requestBody.siteName;
site.url = requestBody.siteUrl;
site.icon = requestBody.siteIcon;
site.info = requestBody.siteInfo || '';
// 如果分类发生变化,移动站点到新分类
if (originalCategoryIndex !== newCategoryIndex) {
// 从原分类移除
originalCategory.sites.splice(siteIndex, 1);
// 添加到新分类
navigationData.categories[newCategoryIndex].sites.push(site);
}
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '站点更新成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
async function deleteCategory(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 验证索引有效性
if (requestBody.categoryIndex < 0 ||
requestBody.categoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
navigationData.categories.splice(requestBody.categoryIndex, 1);
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '分类删除成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
async function deleteSite(request) {
const requestBody = await request.json();
const navigationData = await getNavigationData();
// 验证索引有效性
if (requestBody.categoryIndex < 0 ||
requestBody.categoryIndex >= navigationData.categories.length) {
return new Response(JSON.stringify({ error: '无效分类索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const category = navigationData.categories[requestBody.categoryIndex];
if (requestBody.siteIndex < 0 || requestBody.siteIndex >= category.sites.length) {
return new Response(JSON.stringify({ error: '无效站点索引' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
category.sites.splice(requestBody.siteIndex, 1);
await setNavigationData(navigationData);
return new Response(JSON.stringify({ message: '站点删除成功' }), {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
async function loadNotification() {
const notification = await NAVIGATION_DATA.get('info');
return new Response(notification || '', {
headers: { 'Cache-Control': 'no-cache' }
});
}
// 页面渲染函数
async function renderLoginPage(showError = false) {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dispark|点滴星火登录页</title>
<style>
body {
margin: 0;
height: 100vh;
background-image: url('https://cdn.pixabay.com/photo/2024/09/06/02/03/hill-9026381_960_720.png');
background-size: cover;
background-position: center;
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
h1 {
position: absolute;
top: 20px;
left: 20px;
font-size: 36px;
margin: 0;
color: #fff;
}
.login-container {
background-color: rgba(255, 255, 255, 0.8);
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 350px;
text-align: center;
}
.login-container input {
width: 100%;
padding: 10px;
margin: 5px 0 15px 0;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.5);
}
.login-container button {
width: 100%;
padding: 12px;
background-color: #007BFF;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.login-container button:hover {
background-color: #0056b3;
}
footer {
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
font-size: 14px;
color: #fff;
}
footer a {
color: #fff;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.error-message {
color: #ff0000;
margin-bottom: 15px;
display: ${showError ? 'block' : 'none'};
}
</style>
</head>
<body>
<h1 style="display: flex; align-items: center; justify-content: center; gap: 10px; font-size: 24px;">
<a href="https://www.9872991.xyz/">
<img src="https://api.iconify.design/devicon/prometheus.svg?width=64&height=64&color=%23FFFFFF"
alt="Prometheus Icon" style="filter: invert(1);">
</a>
<span>Dispark|点滴星火</span>
</h1>
<div class="login-container">
<div class="error-message">用户名或密码错误,请重试</div>
<form action="/login" method="POST">
<input type="text" id="username" name="username" placeholder="用户名" required>
<input type="password" id="password" name="password" placeholder="密码" required>
<button type="submit" id="login-button">登录</button>
</form>
</div>
<footer>
<p>
<span>© 2025</span>
<a href="https://www.9912987.xyz/" target="_blank">Dispark|点滴星火</a>
<a href="/privacy">隐私政策</a>
<a href="mailto:i@dispark.top">联系方式</a>
<a href="https://wwww.9872991.xyz">更多书签</a>
</p>
</footer>
</body>
</html>`;
}
async function renderNavigationPage() {
const navigationData = await getNavigationData();
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dispark|点滴星火</title>
<link rel="icon" href="https://api.iconify.design/devicon/prometheus.svg" type="image/png">
<script src="https://code.iconify.design/2/2.0.3/iconify.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background-color: #f4f4f9;
color: #333;
margin: 0;
}
/* 搜索框容器 - 顶部居中位置 */
.search-container {
position: relative;
max-width: 200px;
margin: 0 auto;
padding: 15px 0;
}
/* 搜索框样式 */
#searchInput {
width: 100%;
padding: 14px 20px 14px 50px;
font-size: 16px;
border: none;
border-radius: 50px;
background: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
color: #333;
outline: none;
}
#searchInput:focus {
box-shadow: 0 4px 16px rgba(74, 144, 226, 0.3);
transform: translateY(-2px);
}
/* 搜索图标 */
.search-icon {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
color: #999;
pointer-events: none;
transition: color 0.3s;
}
#searchInput:focus + .search-icon {
color: #4A90E2;
}
/* 清除按钮 */
.clear-btn {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #999;
cursor: pointer;
display: none;
}
#searchInput:not(:placeholder-shown) + .search-icon + .clear-btn {
display: block;
}
/* 搜索结果计数 */
.search-results {
text-align: center;
margin: 10px 0 20px;
color: #666;
font-size: 14px;
font-weight: 500;
}
/* 优化后的 header 样式 */
header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
width: 100%;
background-color: #fff;
z-index: 100;
padding: 10px 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
/* 添加拖拽样式 */
.drag-handle {
cursor: move;
opacity: 0.5;
transition: opacity 0.3s;
margin-right: 5px;
}
.drag-handle:hover {
opacity: 1;
}
.dragging {
opacity: 0.7;
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* 移动端优化 - 关键改进 */
@media (max-width: 768px) {
header {
flex-direction: column;
padding: 5px 8px; /* 更紧凑的内边距 */
}
header > div {
width: 100%;
max-width: 100% !important;
min-width: 100% !important;
margin-bottom: 10px;
}
.search-container {
max-width: 100% !important;
}
.header-buttons {
justify-content: center !important;
}
.header-top {
flex-direction: column;
align-items: center;
width: 100%;
}
.header-left {
text-align: center;
margin-bottom: 5px;
}
.header-left h1 {
font-size: 1.1rem !important; /* 更小的字体 */
}
.main-title {
font-size: 1.2rem !important; /* 更小的标题 */
margin: 3px 0 !important;
}
.title-subtext {
font-size: 0.6rem !important; /* 更小的副标题 */
margin-top: 0 !important;
}
#datetime {
font-size: 0.7rem;
margin: 2px 0;
}
.header-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5px;
width: 100%;
margin: 5px 0 0 0 !important;
}
.header-buttons button {
padding: 5px 8px;
font-size: 11px;
min-height: 32px;
}
/* 滚动时进一步缩小 header */
body.scrolled header {
padding: 3px 5px;
height: 50px; /* 固定高度 */
overflow: hidden;
}
body.scrolled .header-middle {
display: none; /* 滚动时隐藏中间部分 */
}
body.scrolled .header-top {
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
body.scrolled .header-left {
flex: 1;
text-align: left;
margin-bottom: 0;
}
body.scrolled .header-buttons {
display: none; /* 滚动时隐藏按钮 */
}
/* 调整主内容区域 */
main {
margin-top: 120px; /* 更小的初始高度 */
transition: margin-top 0.3s ease;
}
body.scrolled main {
margin-top: 60px; /* 滚动后更小的空间 */
}
footer {
font-size: 12px;
flex-direction: column;
height: auto;
padding: 8px 0;
}
footer p {
flex-direction: column;
gap: 5px;
}
footer a::after {
content: none;
}
}
/* 桌面端优化 */
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.header-middle {
display: flex;
flex-direction: column;
align-items: center;
margin: 5px 0;
transition: all 0.3s ease;
}
.header-buttons {
display: flex;
gap: 10px;
margin-right: 20px;
transition: all 0.3s ease;
}
/* 滚动时缩小 header */
body.scrolled header {
padding: 5px 15px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
body.scrolled .main-title {
font-size: 2rem;
}
body.scrolled .title-subtext {
font-size: 0.8rem;
}
body.scrolled .header-buttons button {
padding: 6px 12px;
}
/* 主内容区域调整 */
main {
margin-top: 140px;
transition: margin-top 0.3s ease;
}
body.scrolled main {
margin-top: 70px;
}
main {
margin-top: 80px;
padding: 20px;
}
section {
margin-bottom: 30px;
}
h2 {
margin: 0;
font-size: 1.5rem;
color: #4A90E2;
border-bottom: 2px solid #4A90E2;
padding-bottom: 5px;
display: flex;
align-items: center;
}
ul {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
gap: 10px;
}
li {
width: 120px;
padding: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
text-align: center;
transition: all 0.3s ease;
position: relative;
}
li:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.item-icon {
cursor: pointer;
}
a {
display: block;
margin-top: 8px;
color: #333;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-decoration: none;
}
a:hover {
color: #ff6700;
}
.opts {
margin-top: 8px;
display: flex;
justify-content: center;
gap: 5px;
}
button {
cursor: pointer;
background: none;
border: none;
padding: 0;
}
#addCategoryForm {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
gap: 15px;
}
#notification-banner {
flex: 1;
overflow: hidden;
white-space: nowrap;
}
#notification-message {
display: inline-block;
padding-left: 100%;
animation: scroll-left 20s linear infinite;
}
@keyframes scroll-left {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
/* 新增标题样式 */
.main-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(45deg, #4A90E2, #5E60CE, #7B68EE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
text-shadow: 0 4px 10px rgba(74, 144, 226, 0.2);
letter-spacing: 1px;
position: relative;
display: inline-block;
margin: 0 0 10px 0;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
.main-title::after {
content: "";
position: absolute;
bottom: -10px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #4A90E2, #7B68EE);
border-radius: 2px;
transform: scaleX(0.8);
}
.title-subtext {
font-size: 1rem;
color: #666;
font-weight: 400;
letter-spacing: 2px;
text-transform: uppercase;
margin-top: 5px;
opacity: 0.8;
}
/* 添加标题动画 */
@keyframes titleGlow {
0% { text-shadow: 0 4px 10px rgba(74, 144, 226, 0.2); }
50% { text-shadow: 0 4px 20px rgba(123, 104, 238, 0.4); }
100% { text-shadow: 0 4px 10px rgba(74, 144, 226, 0.2); }
}
.main-title {
animation: titleGlow 4s ease-in-out infinite;
}
/* 修改模态框为不透明背景 */
.modal-content {
background: #fff; /* 改为纯白色不透明背景 */
padding: 25px;
border-radius: 10px;
width: 90%;
max-width: 500px;
position: relative;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
/* 添加分类模态框特定样式 */
/* 优化添加分类模态框为水平布局 */
#addCategoryModal .modal-content {
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
padding: 25px;
max-width: 500px;
width: 90%;
}
#addCategoryModal h2 {
font-size: 20px;
color: #333;
margin-top: 0;
margin-bottom: 20px;
font-weight: 600;
text-align: center;
}
#addCategoryForm {
display: flex;
align-items: center;
gap: 10px;
}
#addCategoryForm input {
flex: 1;
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 15px;
transition: all 0.3s;
height: 46px;
box-sizing: border-box;
}
#addCategoryForm input:focus {
border-color: #ff9800;
box-shadow: 0 0 0 2px rgba(255, 152, 0, 0.2);
outline: none;
}
#addCategoryForm button[type="submit"] {
background: #ff9800;
color: white;
border: none;
border-radius: 6px;
padding: 0 20px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
height: 46px;
min-width: 100px;
}
#addCategoryForm button[type="submit"]:hover {
background: #e68a00;
}
/* 所有模态框的统一关闭按钮样式 */
.modal-close {
position: absolute;
top: 15px;
right: 15px;
width: 24px;
height: 24px;
cursor: pointer;
opacity: 0.6;
transition: all 0.3s;
}
.modal-close:hover {
opacity: 1;
transform: rotate(90deg);
}
.modal-close::before, .modal-close::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 18px;
height: 2px;
background: #666;
transform-origin: center;
}
.modal-close::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.modal-close::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
/* 添加站点和编辑站点模态框的样式 */
#myModal .modal-content,
#editSiteModal .modal-content {
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
padding: 25px;
max-width: 500px;
width: 90%;
}
#myModal h2,
#editSiteModal h2 {
font-size: 20px;
color: #333;
margin-top: 0;
margin-bottom: 20px;
font-weight: 600;
}
#addSiteForm,
#editSiteForm {
display: flex;
flex-direction: column;
gap: 15px;
}
#addSiteForm input,
#addSiteForm select,
#addSiteForm textarea,
#editSiteForm input,
#editSiteForm select,
#editSiteForm textarea {
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 15px;
transition: all 0.3s;
}
#addSiteForm input:focus,
#addSiteForm select:focus,
#addSiteForm textarea:focus,
#editSiteForm input:focus,
#editSiteForm select:focus,
#editSiteForm textarea:focus {
border-color: #4A90E2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
outline: none;
}
#addSiteForm button[type="submit"],
#editSiteForm button[type="submit"] {
background: #4A90E2;
color: white;
border: none;
border-radius: 6px;
padding: 12px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
#addSiteForm button[type="submit"]:hover {
background: #3a7bc8;
}
#editSiteForm button[type="submit"] {
background: #ff9800;
}
#editSiteForm button[type="submit"]:hover {
background: #e68a00;
}
form {
display: grid;
gap: 10px;
}
input, select, textarea {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: inherit;
background: #f9f9f9;
}
button[type="submit"] {
padding: 10px;
background: #007BFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
/* 新增悬停提示框样式 */
/* 添加站点描述提示框的悬停样式 */
.info-icon {
position: absolute;
top: 5px;
right: 5px;
color: #999;
cursor: help;
font-size: 12px;
opacity: 0.7;
transition: all 0.3s;
z-index: 10;
}
.info-icon:hover {
opacity: 1;
color: #4A90E2;
}
.info-tooltip {
visibility: hidden;
position: absolute;
top: 100%;
right: 0;
background-color: #333;
color: #fff;
padding: 10px 15px;
border-radius: 6px;
white-space: normal;
width: max-content;
max-width: 280px;
font-size: 14px;
line-height: 1.5;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 100;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.info-icon:hover .info-tooltip {
visibility: visible;
opacity: 1;
transform: translateY(0);
}
.info-tooltip::after {
content: "";
position: absolute;
bottom: 100%;
right: 10px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #333 transparent;
}
/* 修复站点链接点击问题 */
.item-icon {
cursor: pointer;
}
.site-link {
display: block;
text-decoration: none;
color: inherit;
}
.site-name {
display: block;
margin-top: 8px;
color: #333;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.site-name:hover {
color: #ff6700;
}
/* 自定义确认对话框样式 */
#customConfirmModal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
.confirm-content {
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
padding: 25px;
max-width: 400px;
width: 90%;
text-align: center;
}
.confirm-message {
font-size: 18px;
margin-bottom: 25px;
color: #333;
}
.confirm-buttons {
display: flex;
justify-content: center;
gap: 15px;
}
.confirm-button {
padding: 10px 25px;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.confirm-ok {
background: #ff5252;
color: white;
}
.confirm-ok:hover {
background: #e04545;
}
.confirm-cancel {
background: #e0e0e0;
color: #333;
}
.confirm-cancel:hover {
background: #d0d0d0;
}
/* 页脚样式优化 - 开始 */
footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px; /* 增加高度 */
background: #f0f0f0;
text-align: center;
font-size: 13px;
z-index: 999;
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
}
footer p {
margin: 0;
padding: 0 15px;
display: flex;
align-items: center;
gap: 10px;
}
footer a {
color: #3498db;
text-decoration: none;
transition: color 0.2s;
display: inline-flex;
align-items: center;
}
footer a:hover {
color: #2980b9;
text-decoration: underline;
}
footer a::after {
content: "|";
margin-left: 10px;
color: #ccc;
}
footer a:last-child::after {
content: none;
}
/* 页脚样式优化 - 结束 */
</style>
</head>
<body>
<header style="display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 15px; background: rgba(255,255,255,0.95); box-shadow: 0 2px 10px rgba(0,0,0,0.05); position: fixed; top: 0; left: 0; z-index: 1000; box-sizing: border-box;">
<!-- 左侧Logo和搜索框组合 -->
<div style="display: flex; align-items: center; flex: 1; min-width: 40%;">
<!-- Logo和标题 -->
<div style="display: flex; flex-direction: column; margin-right: 20px; flex-shrink: 0;">
<div style="display: flex; align-items: center;">
<a href="https://www.9872991.xyz/" target="_blank" style="display: flex; align-items: center;">
<img src="https://api.iconify.design/devicon/prometheus.svg?width=48&height=48" alt="Logo">
<h1 style="margin-left: 10px; margin-bottom: 0; font-size: 1.5rem; color: #333; white-space: nowrap;">Dispark</h1>
</a>
</div>
<div id="datetime" style="font-size: 0.9rem; margin-top: 5px; color: #666; white-space: nowrap;">
正在加载时间...
</div>
</div>
<!-- 搜索框 - 靠近Logo -->
<div class="search-container" style="flex: 1; min-width: 30px; max-width: 300px;">
<input type="text" id="searchInput" placeholder="搜索站点名称或描述...">
<span class="search-icon iconify" data-icon="mdi:magnify" data-width="24" data-height="24"></span>
<button class="clear-btn" onclick="clearSearch()">
<span class="iconify" data-icon="mdi:close" data-width="20" data-height="20"></span>
</button>
</div>
</div>
<!-- 右侧:主标题和按钮 -->
<div style="display: flex; flex-direction: column; align-items: center; flex: 1; min-width: 20%; overflow: hidden;">
<!-- 主标题 -->
<div class="main-title" style="font-size: 1.8rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">点滴星火导航站</div>
<div class="title-subtext" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">探索 · 连接 · 发现</div>
</div>
<!-- 右侧按钮组 -->
<div style="display: flex; gap: 10px; flex-shrink: 1; flex-wrap: wrap; justify-content: flex-end; max-width: 40%;">
<button id="addCategoryBtn" style="background-color: #ff9800; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<span class="iconify" data-icon="carbon:add-filled" data-width="16px" data-height="16px"></span>
添加分类
</button>
<button onclick="window.open('https://xinsheng.9912987.xyz/', '_blank')" style="background-color: #28a745; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<span class="iconify" data-icon="mdi:message-text" data-width="16px" data-height="16px"></span>
留言板
</button>
<button onclick="window.open('https://news.9912987.xyz/', '_blank')" style="background-color: #28a745; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<span class="iconify" data-icon="mdi:newspaper" data-width="16px" data-height="16px"></span>
新闻
</button>
<!-- 导出按钮 -->
<button onclick="exportData()" style="background-color: #17a2b8; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<span class="iconify" data-icon="mdi:export" data-width="16px" data-height="16px"></span>
导出
</button>
<!-- 导入按钮(触发隐藏 file input -->
<button onclick="document.getElementById('importFile').click()" style="background-color: #6c757d; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<span class="iconify" data-icon="mdi:import" data-width="16px" data-height="16px"></span>
导入
</button>
<input type="file" id="importFile" accept=".json,application/json" style="display: none;" onchange="importData(this.files[0])">
<button onclick="location.href='/login.html'" style="background-color: #007BFF; color: white; border: none; padding: 8px 12px; border-radius: 5px; cursor: pointer; display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<span class="iconify" data-icon="mdi:logout" data-width="16px" data-height="16px"></span>
退出
</button>
</div>
</header>
<main>
<!-- 搜索结果计数显示 -->
<div class="search-results" id="searchResults"></div>
${renderCategories(navigationData.categories)} <!-- 确保这里调用了renderCategories -->
</main>
<!-- 自定义确认对话框 -->
<div id="customConfirmModal">
<div class="confirm-content">
<div class="confirm-message" id="confirmMessage"></div>
<div class="confirm-buttons">
<button class="confirm-button confirm-ok" id="confirmOk">确定</button>
<button class="confirm-button confirm-cancel" id="confirmCancel">取消</button>
</div>
</div>
</div>
<!-- 修复后的添加分类模态框 -->
<div id="addCategoryModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-close" onclick="closeModal('addCategoryModal')"></div>
<h2>添加新分类</h2>
<form id="addCategoryForm">
<input type="text" name="categoryName" placeholder="输入分类名称" required>
<button type="submit">添加分类</button>
</form>
</div>
</div>
<!-- 添加站点模态框 -->
<div id="myModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-close" onclick="closeModal('myModal')"></div>
<h2>添加新站点</h2>
<form id="addSiteForm">
<select name="categoryIndex" required>
${navigationData.categories.map((category, index) =>
`<option value="${index}">${category.name}</option>`
).join('')}
</select>
<input type="text" name="siteName" placeholder="站点名称" required>
<input type="url" name="siteUrl" placeholder="站点链接" required>
<input type="text" name="siteIcon" placeholder="图标名称" required value="noto:fire">
<textarea name="siteInfo" placeholder="站点描述 (可选)" rows="3"></textarea>
<button type="submit">添加站点</button>
</form>
</div>
</div>
<!-- 添加分类名称修改模态框到HTML -->
<div id="editCategoryModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-close" onclick="closeModal('editCategoryModal')"></div>
<h2>修改分类名称</h2>
<form id="editCategoryForm">
<input type="hidden" name="categoryIndex">
<input type="text" name="categoryName" placeholder="分类名称" required>
<button type="submit">保存修改</button>
</form>
</div>
</div>
<!-- 在编辑站点模态框中添加分类选择 -->
<!-- 编辑站点模态框 -->
<div id="editSiteModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-close" onclick="closeModal('editSiteModal')"></div>
<h2>修改站点</h2>
<form id="editSiteForm">
<input type="hidden" name="originalCategoryIndex">
<input type="hidden" name="siteIndex">
<!-- 添加分类选择下拉菜单 -->
<select name="newCategoryIndex" required>
${navigationData.categories.map((category, index) =>
`<option value="${index}">${category.name}</option>`
).join('')}
</select>
<input type="text" name="siteName" placeholder="站点名称" required>
<input type="url" name="siteUrl" placeholder="站点链接" required>
<input type="text" name="siteIcon" placeholder="图标名称" required>
<textarea name="siteInfo" placeholder="站点描述 (可选)" rows="3"></textarea>
<button type="submit">保存修改</button>
</form>
</div>
</div>
<script>
// 在script标签中添加拖拽功能
let draggedItem = null;
function onDragStart(event, categoryIndex, siteIndex) {
event.dataTransfer.setData("text/plain", JSON.stringify({
categoryIndex: categoryIndex,
siteIndex: siteIndex
}));
event.currentTarget.classList.add('dragging');
draggedItem = event.currentTarget;
}
function onDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}
async function onDrop(event, targetCategoryIndex, targetSiteIndex) {
event.preventDefault();
if (!draggedItem) return;
const data = JSON.parse(event.dataTransfer.getData("text/plain"));
const sourceCategoryIndex = data.categoryIndex;
const sourceSiteIndex = data.siteIndex;
// 移除拖拽样式
draggedItem.classList.remove('dragging');
draggedItem = null;
// 如果是同一分类内的移动
if (sourceCategoryIndex === targetCategoryIndex) {
try {
const response = await fetch('/reorder-site', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
categoryIndex: sourceCategoryIndex,
oldIndex: sourceSiteIndex,
newIndex: targetSiteIndex
})
});
if (response.ok) {
location.reload();
} else {
const error = await response.json();
alert(error.error || '移动失败');
}
} catch (error) {
console.error('移动失败:', error);
alert('站点移动失败,请重试');
}
} else {
alert('站点只能在同一个分类内移动位置');
}
}
// 添加切换分类折叠状态函数
async function toggleCategoryState(categoryIndex) {
const response = await fetch('/toggle-category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ categoryIndex })
});
if (response.ok) {
const result = await response.json();
const categoryElement = document.getElementById('category-sites-' + categoryIndex);
const toggleIcon = document.getElementById('toggle-icon-' + categoryIndex);
if (result.collapsed) {
categoryElement.style.display = 'none';
toggleIcon.setAttribute('data-icon', 'mdi:chevron-right');
} else {
categoryElement.style.display = 'flex';
toggleIcon.setAttribute('data-icon', 'mdi:chevron-down');
}
}
}
// 修复站点链接点击问题
function openSiteLink(url) {
window.open(url, '_blank');
}
// 自定义确认函数
let confirmCallback = null;
let currentCategoryIndex = null;
let currentSiteIndex = null;
let isSiteDeletion = false;
function showConfirm(message, callback, categoryIndex, siteIndex = null) {
document.getElementById('confirmMessage').textContent = message;
confirmCallback = callback;
currentCategoryIndex = categoryIndex;
currentSiteIndex = siteIndex;
isSiteDeletion = siteIndex !== null;
document.getElementById('customConfirmModal').style.display = "flex";
}
// 打开编辑分类模态框
function openEditCategoryModal(categoryIndex) {
const categoryName = document.getElementById(\`category-name-\${categoryIndex}\`).textContent;
document.getElementById('editCategoryForm').categoryIndex.value = categoryIndex;
document.getElementById('editCategoryForm').categoryName.value = categoryName;
document.getElementById('editCategoryModal').style.display = "flex";
}
// 添加编辑分类表单提交事件
document.getElementById('editCategoryForm')?.addEventListener('submit', async function(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
categoryIndex: parseInt(formData.get('categoryIndex')),
newName: formData.get('categoryName')
};
const response = await fetch('/edit-category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal('editCategoryModal');
// 更新分类名称显示
const categoryNameElement = document.getElementById(\`category-name-\${data.categoryIndex}\`);
if (categoryNameElement) {
categoryNameElement.textContent = data.newName;
}
} else {
const error = await response.json();
alert(error.error || '修改失败');
}
});
// 绑定确认对话框按钮事件
document.getElementById('confirmOk').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback(currentCategoryIndex, currentSiteIndex);
}
document.getElementById('customConfirmModal').style.display = "none";
});
document.getElementById('confirmCancel').addEventListener('click', function() {
document.getElementById('customConfirmModal').style.display = "none";
});
// 修改删除分类函数 - 修复字符串问题
function deleteCategory(categoryIndex) {
const categoryName = navigationData.categories[categoryIndex]?.name || '该分类';
showConfirm('确定要删除分类 "' + categoryName + '" 吗?此操作不可恢复。',
performDeleteCategory,
categoryIndex);
}
async function performDeleteCategory(categoryIndex) {
const response = await fetch('/delete-category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ categoryIndex })
});
if (response.ok) location.reload();
}
// 修改删除站点函数 - 修复字符串问题
function deleteSite(categoryIndex, siteIndex) {
const siteName = navigationData.categories[categoryIndex]?.sites[siteIndex]?.name || '该站点';
showConfirm('确定要删除站点 "' + siteName + '" 吗?此操作不可恢复。',
performDeleteSite,
categoryIndex,
siteIndex);
}
async function performDeleteSite(categoryIndex, siteIndex) {
const response = await fetch('/delete-site', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ categoryIndex, siteIndex })
});
if (response.ok) location.reload();
}
// 初始化导航数据(从服务器获取)
let navigationData = { categories: [] };
// 关闭模态框函数
function closeModal(modalId) {
document.getElementById(modalId).style.display = "none";
}
// 添加分类按钮事件
document.getElementById('addCategoryBtn')?.addEventListener('click', function() {
document.getElementById('addCategoryModal').style.display = "flex";
});
// 添加分类表单提交事件
document.getElementById('addCategoryForm')?.addEventListener('submit', async function(event) {
event.preventDefault();
const categoryName = event.target.categoryName.value;
const response = await fetch('/data');
const navigationData = await response.json();
const categoryExists = navigationData.categories.some(
cat => cat.name === categoryName
);
if (!categoryExists) {
const addResponse = await fetch('/add-category', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: categoryName })
});
if (addResponse.ok) {
document.getElementById('addCategoryModal').style.display = "none";
location.reload();
}
} else {
alert('该分类已经存在,请输入不同的分类名称。');
}
});
// 添加站点表单提交事件
document.getElementById('addSiteForm')?.addEventListener('submit', async function(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
categoryIndex: parseInt(formData.get('categoryIndex')),
siteName: formData.get('siteName'),
siteUrl: formData.get('siteUrl'),
siteIcon: formData.get('siteIcon'),
siteInfo: formData.get('siteInfo')
};
const response = await fetch('/add-site', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal('myModal');
location.reload();
}
});
// 编辑站点表单提交事件
document.getElementById('editSiteForm')?.addEventListener('submit', async function(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
originalCategoryIndex: parseInt(formData.get('originalCategoryIndex')),
newCategoryIndex: parseInt(formData.get('newCategoryIndex')),
siteIndex: parseInt(formData.get('siteIndex')),
siteName: formData.get('siteName'),
siteUrl: formData.get('siteUrl'),
siteIcon: formData.get('siteIcon'),
siteInfo: formData.get('siteInfo')
};
const response = await fetch('/edit-site', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal('editSiteModal');
location.reload();
}
});
// 模态框控制
const modal = document.getElementById("myModal");
const editModal = document.getElementById("editSiteModal");
const infoModal = document.getElementById("infoModal");
// 打开添加站点模态框的函数
function openAddModal(categoryIndex) {
document.getElementById('myModal').style.display = "flex";
document.querySelector("#addSiteForm select").value = categoryIndex;
}
// 打开编辑站点模态框的函数
function openEditModal(categoryIndex, siteIndex, siteName, siteUrl, siteIcon, siteInfo) {
const modal = document.getElementById('editSiteModal');
modal.style.display = "flex";
const form = document.getElementById('editSiteForm');
form.originalCategoryIndex.value = categoryIndex;
form.newCategoryIndex.value = categoryIndex;
form.siteIndex.value = siteIndex;
form.siteName.value = siteName;
form.siteUrl.value = siteUrl;
form.siteIcon.value = siteIcon;
form.siteInfo.value = siteInfo || '';
}
// 显示站点描述
function showSiteInfo(siteName, siteInfo) {
if (!siteInfo) return;
document.getElementById('info-title').textContent = siteName + ' - 描述';
document.getElementById('info-text').textContent = siteInfo;
infoModal.style.display = "flex";
}
// 关闭所有模态框
function closeModals() {
document.getElementById('myModal').style.display = "none";
document.getElementById('editSiteModal').style.display = "none";
document.getElementById('infoModal').style.display = "none";
document.getElementById('addCategoryModal').style.display = "none";
}
// 关闭按钮事件
document.querySelectorAll('.close, .info-close').forEach(btn => {
btn.addEventListener('click', closeModals);
});
// 点击模态框外部关闭
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) {
// 关闭所有模态框
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = "none";
});
}
});
// 日期显示
function updateDate() {
const now = new Date();
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};
document.getElementById('datetime').textContent =
now.toLocaleDateString('zh-CN', options);
}
// 添加悬停事件处理函数
function setupTooltips() {
// 为所有信息图标添加悬停事件
document.querySelectorAll('.info-icon').forEach(icon => {
const tooltip = icon.querySelector('.info-tooltip');
icon.addEventListener('mouseenter', () => {
tooltip.style.visibility = 'visible';
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(0)';
});
icon.addEventListener('mouseleave', () => {
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(10px)';
});
});
}
// 优化后的滚动处理
window.addEventListener('scroll', function() {
const scrollPosition = window.scrollY;
const header = document.querySelector('header');
const main = document.querySelector('main');
const isMobile = window.innerWidth <= 768;
if (scrollPosition > 30) {
document.body.classList.add('scrolled');
} else {
document.body.classList.remove('scrolled');
}
// 移动设备上进一步优化
if (isMobile) {
if (scrollPosition > 50) {
header.style.position = 'fixed';
header.style.height = '50px';
main.style.marginTop = '60px';
} else {
header.style.position = 'sticky';
header.style.height = 'auto';
main.style.marginTop = '120px';
}
}
});
// 添加搜索功能
document.getElementById('searchInput').addEventListener('input', function() {
const searchTerm = this.value.trim().toLowerCase();
performSearch(searchTerm);
});
// 清除搜索
function clearSearch() {
document.getElementById('searchInput').value = '';
performSearch('');
}
// 搜索执行函数
function performSearch(term) {
const resultsContainer = document.getElementById('searchResults');
// 清除之前的高亮
document.querySelectorAll('.highlight').forEach(el => {
const parent = el.parentNode;
parent.replaceChild(document.createTextNode(el.textContent), el);
});
// 重置所有元素的显示状态
document.querySelectorAll('section').forEach(section => {
section.style.display = 'block';
});
document.querySelectorAll('section ul li').forEach(li => {
li.style.display = 'flex';
});
// 如果没有搜索词,显示所有内容
if (!term) {
resultsContainer.textContent = '';
return;
}
// 搜索逻辑
let matchCount = 0;
// 遍历所有站点
document.querySelectorAll('section ul li').forEach(li => {
const nameElement = li.querySelector('.site-name');
const infoElement = li.querySelector('.info-tooltip');
const name = nameElement ? nameElement.textContent.toLowerCase() : '';
const info = infoElement ? infoElement.textContent.toLowerCase() : '';
// 检查是否匹配
if (name.includes(term) || info.includes(term)) {
matchCount++;
// 高亮匹配文本
if (name.includes(term)) {
highlightText(nameElement, term);
}
if (info.includes(term) && infoElement) {
highlightText(infoElement, term);
}
} else {
// 隐藏不匹配的站点
li.style.display = 'none';
}
});
// 隐藏没有匹配站点的分类
document.querySelectorAll('section').forEach(section => {
const visibleSites = section.querySelectorAll('ul li[style="display: flex;"]');
if (visibleSites.length === 0) {
section.style.display = 'none';
}
});
// 显示结果计数
resultsContainer.textContent = matchCount > 0 ?
'找到 ' + matchCount + ' 个匹配结果' :
'没有找到匹配的站点';
// 如果有匹配结果,滚动到第一个结果
if (matchCount > 0) {
const firstMatch = document.querySelector('section:not([style="display: none;"])');
if (firstMatch) {
firstMatch.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}
// 导出数据
async function exportData() {
// 直接跳转到导出接口,浏览器会自动下载
window.location.href = '/export';
}
// 导入数据
async function importData(file) {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/import', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (response.ok) {
alert('导入成功,页面即将刷新');
location.reload();
} else {
alert('导入失败:' + (result.error || '未知错误'));
}
}
// 高亮文本函数
function highlightText(element, term) {
const text = element.textContent;
const regex = new RegExp('(' + escapeRegExp(term) + ')', 'gi');
const newContent = text.replace(regex, '<span class="highlight">$1</span>');
element.innerHTML = newContent;
}
// 转义正则表达式特殊字符
function escapeRegExp(string) {
return string.replace(/[.*+?^$()|[\]\\]/g, '\\$&');
}
// 初始化
window.addEventListener('load', async function() {
updateDate();
// 加载通知
const response = await fetch('/load-notification');
const notification = await response.text();
document.getElementById('notification-message').textContent =
notification || '暂无新通知';
// 设置工具提示
// setupTooltips();
// 移动设备上设置更紧凑的布局
if (window.innerWidth <= 768) {
document.querySelector('header').classList.add('mobile');
document.querySelector('main').style.marginTop = '120px';
}
});
</script>
<footer>
<p>
<a>© 2025</a>
<a href="https://www.9912987.xyz/" target="_blank">Dispark|点滴星火</a>
<a href="/privacy">隐私政策</a>
<a href="mailto:i@dispark.top">联系方式</a>
<a href="http://113.44.203.109:36615" target="_blank" rel="noopener noreferrer">更多书签</a>
</p>
</footer>
</body>
</html>`;
return html;
}
/// 修复renderCategories函数 - 关键修改
function renderCategories(categories) {
return categories.map((category, categoryIndex) => `
<section>
<h2 style="display: flex; align-items: center;">
<!-- 折叠/展开按钮 -->
<button class="toggle-btn" onclick="toggleCategoryState(${categoryIndex})"
style="background: none; border: none; cursor: pointer; margin-right: 8px;">
<span id="toggle-icon-${categoryIndex}" class="iconify"
data-icon="${category.collapsed ? 'mdi:chevron-right' : 'mdi:chevron-down'}"
data-width="24px" data-height="24px"></span>
</button>
<span id="category-name-${categoryIndex}">${category.name}</span>
<!-- 编辑分类名称按钮 -->
<button class="edit-category-btn" onclick="openEditCategoryModal(${categoryIndex})"
style="background: none; border: none; cursor: pointer; margin-left: 8px;">
<span class="iconify" data-icon="mdi:pencil" data-width="20px" data-height="20px"></span>
</button>
<!-- 删除分类按钮 -->
<button class="delete-ca-btn" onclick="deleteCategory(${categoryIndex})">
<span class="iconify" data-icon="material-symbols:delete-outline" data-width="24px" data-height="24px"></span>
</button>
<!-- 添加站点按钮 -->
<button class="add-site-btn" onclick="openAddModal(${categoryIndex})">
<span class="iconify" data-icon="carbon:add-filled" data-width="24px" data-height="24px"></span>
</button>
</h2>
<ul id="category-sites-${categoryIndex}"
style="${category.collapsed ? 'display: none;' : 'display: flex;'}">
${category.sites.map((site, siteIndex) => `
<li draggable="true" ondragstart="onDragStart(event, ${categoryIndex}, ${siteIndex})" ondragover="onDragOver(event)" ondrop="onDrop(event, ${categoryIndex}, ${siteIndex})">
<div class="site-block" onclick="openSiteLink('${escapeString(site.url)}')">
<!-- 添加拖拽手柄 -->
<div class="drag-handle" onclick="event.stopPropagation()">
<span class="iconify" data-icon="mdi:drag" data-width="16px" data-height="16px"></span>
</div>
<div class="item-icon">
<span class="iconify" data-icon="${site.icon}" data-width="40px" data-height="40px"></span>
</div>
<a href="${site.url}" target="_blank" class="site-link" onclick="event.stopPropagation(); return true;">
<span class="site-name">${site.name}</span>
</a>
${site.info ? `
<div class="info-icon" onclick="event.stopPropagation()">
<span class="iconify" data-icon="mdi:information-outline" data-width="16px" data-height="16px"></span>
<div class="info-tooltip">
${site.info}
</div>
</div>
` : ''}
<div class="opts">
<button onclick="event.stopPropagation(); deleteSite(${categoryIndex}, ${siteIndex})">
<span class="iconify" data-icon="material-symbols:delete-outline" data-width="16px" data-height="16px"></span>
</button>
<button onclick="event.stopPropagation(); openEditModal(${categoryIndex}, ${siteIndex}, '${escapeString(site.name)}', '${escapeString(site.url)}', '${escapeString(site.icon)}', '${escapeString(site.info || '')}')">
<span class="iconify" data-icon="raphael:edit" data-width="16px" data-height="16px"></span>
</button>
</div>
</div>
</li>
`).join('')}
</ul>
</section>
`).join('');
}
// 辅助函数:转义特殊字符
function escapeString(str) {
return str ? str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r') : '';
}