AIDaohang/templates/index.html
2025-07-11 16:54:33 +08:00

2090 lines
68 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>应用导航</title>
{% if settings and settings.logo_type == 'image' and settings.logo_image %}
<link rel="icon" href="{{ settings.logo_image }}">
{% else %}
<link rel="icon" href="/static/favicon.png">
{% endif %}
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/all.min.css">
<style>
:root {
--primary-color: #4361ee;
--hover-color: #3a56d4;
--dev-color: #4cc9f0;
--edu-color: #f72585;
--tool-color: #7209b7;
--law-color: #4895ef;
--ai-color: #f8961e;
--tooltip-bg: rgb(149 236 105);
--tooltip-text: black;
--bg-image: none;
--dark-bg-image: none;
--title-transition: color 0.15s ease-in-out;
}
* {
box-sizing: border-box;
}
/* 强制页面内容区域始终显示垂直滚动条 */
html {
overflow-y: scroll;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background-color: #f8f9fa;
color: #333;
transition: background-color 0.15s ease-in-out;
overflow-x: hidden;
width: 100%;
overflow-y: auto;
}
/* 新增私有卡片标记样式 */
.private-badge {
position: absolute;
top: -1px;
right: -1px;
width: 0;
height: 0;
border-style: solid;
border-width: 0 30px 30px 0;
border-color: transparent #f8961e transparent transparent;
border-radius: 0 12px 0 0;
}
.private-badge::after {
content: "私";
position: absolute;
top: 2px;
right: -25px;
color: white;
font-size: 10px;
transform: rotate(45deg);
}
img, svg {
vertical-align: middle;
margin-bottom: 7px !important;
}
/* 背景图片容器 */
.bg-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
/* 视频背景容器 */
.video-bg-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
overflow: hidden;
}
.video-bg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
object-fit: cover;
}
/* 明亮主题背景 */
.bg-light {
background-image: var(--bg-image);
}
/* 暗黑主题背景 */
.bg-dark {
background-image: var(--dark-bg-image);
}
/* 背景加载完成后的状态 */
body.bg-loaded .bg-container {
opacity: 1;
}
/* 暗黑主题下显示暗黑背景 */
body.dark-theme .bg-light {
display: none;
}
body.dark-theme .bg-dark {
display: block;
}
/* 明亮主题下显示明亮背景 */
body:not(.dark-theme) .bg-light {
display: block;
}
body:not(.dark-theme) .bg-dark {
display: none;
}
h1 {
text-align: center;
color: var(--primary-color);
margin-bottom: 30px;
font-weight: 600;
position: relative;
transition: var(--title-transition);
}
/* 移除导航栏按钮样式 */
.nav-header {
margin-bottom: 20px;
}
/* 新增的搜索框样式 */
/* 修改后的搜索框和结果样式 */
.search-container {
margin: 20px auto;
max-width: 500px; /* 缩小搜索框最大宽度 */
position: relative;
width: 100%;
padding: 0 15px;
}
.search-input {
width: 100%;
padding: 12px 20px;
border-radius: 25px;
border: 1px solid #ddd;
font-size: 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
.search-results {
position: absolute;
width: calc(100% - 30px); /* 修改为与搜索框底边宽度一致 */
/* left: 33px; */ /* 确保与搜索框对齐 */
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
z-index: 100;
max-height: 300px;
overflow-y: auto;
display: none;
transition: background-color 0.15s ease-in-out;
scrollbar-width: thin;
scrollbar-color: #939393;
margin-top: 2px;
}
/* 自定义滚动条样式 */
.search-results::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.search-results::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
.search-results::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 3px;
}
.search-result-item {
padding: 10px 20px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
.search-result-item:hover {
background: #f5f5f5;
}
/* 暗黑模式下的搜索结果样式 */
body.dark-theme .search-results {
background-color: #1e1e1e;
color: #e0e0e0;
}
body.dark-theme .search-result-item {
border-bottom-color: #333;
color: #e0e0e0;
}
body.dark-theme .search-result-item:hover {
background-color: #2a2a2a;
}
/* 右下角按钮组 */
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.floating-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: var(--primary-color);
color: white;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
transition: all 0.3s;
text-decoration: none;
}
.floating-btn:hover {
transform: scale(1.1);
}
/* 按钮提示文字 */
.floating-btn::after {
content: attr(title);
position: absolute;
right: 50px;
white-space: nowrap;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px 10px;
border-radius: 5px;
font-size: 14px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.floating-btn:hover::after {
opacity: 1;
}
/* 暗黑模式下的按钮样式 */
body.dark-theme .floating-btn {
background: #444;
color: #e0e0e0;
}
.floating-btn#addBtn {
background-color: var(--primary-color);
color: white;
display: none;
}
.floating-btn#addBtn:hover {
background-color: var(--hover-color);
}
body.dark-theme .floating-btn#addBtn {
background-color: #444;
color: #e0e0e0;
}
/* 简洁模式卡片样式 */
.app-item.compact {
padding: 10px 15px;
height: 50px;
flex-direction: row;
align-items: center;
}
.app-item.compact .app-icon {
margin-right: 15px;
margin-bottom: 0;
width: 30px;
height: 30px;
font-size: 16px;
}
.app-item.compact .app-info {
width: 100%;
}
.app-item.compact .app-title {
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
}
.app-item.compact .app-url,
.app-item.compact .app-tags {
display: none;
}
/* 暗黑主题 */
body.dark-theme {
background-color: #121212;
color: #e0e0e0;
--tooltip-bg: rgba(30, 30, 30, 0.95);
--tooltip-text: #e0e0e0;
}
/* 新增暗黑模式下标题颜色设置 */
body.dark-theme h1 {
color: white !important;
}
body.dark-theme .app-item {
background-color: #1e1e1edb;
border-color: #333333a1;
color: #e0e0e0;
}
body.dark-theme .category-title {
border-bottom-color: #333;
color: #e0e0e0;
}
body.dark-theme .filter-container {
background-color: #121212d4 !important;
color: #e0e0e0;
}
body.dark-theme .secondary-filters-container {
background-color: #12121200 !important;
color: #e0e0e0;
}
body.dark-theme .filter-btn {
background-color: #2d2d2d !important;
color: #e0e0e0 !important;
}
body.dark-theme .search-input {
background-color: #1e1e1e;
color: #e0e0e0;
border-color: #333;
}
body.dark-theme .search-results {
background-color: #1e1e1e;
color: #e0e0e0;
}
body.dark-theme .search-result-item {
border-bottom-color: #333;
color: #e0e0e0;
}
body.dark-theme .search-result-item:hover {
background-color: #2a2a2a;
}
body.dark-theme .floating-btn {
background-color: #444;
color: #e0e0e0;
}
body.dark-theme .floating-btn:hover {
background-color: #555;
}
body.dark-theme .app-url {
color: #aaa !important;
}
/* 修改后的筛选容器样式 */
.filter-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
position: sticky;
top: 0;
background-color: rgb(255 255 255 / 34%);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
z-index: 10;
padding: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: background-color 0.15s ease-in-out;
margin-left: -15px;
margin-right: -15px;
padding-left: 20px;
padding-right: 20px;
width: calc(100% + 30px);
}
/* 修改后的筛选行样式 */
.filter-row {
display: flex;
flex-wrap: nowrap;
gap: 10px;
overflow-x: auto;
padding-bottom: 10px;
scrollbar-width: thin;
scrollbar-color: #c2c3c9d6;
justify-content: center;
padding: 0 15px;
scroll-behavior: smooth;
position: relative;
margin-left: -10px;
margin-right: -10px;
width: calc(100% + 20px);
}
/* 当内容超出时靠左 */
.filter-row.scrollable {
justify-content: flex-start;
}
/* 新增的滚动指示器 */
.filter-row::before,
.filter-row::after {
content: '';
position: sticky;
top: 0;
width: 40px;
height: 100%;
z-index: 1;
pointer-events: none;
}
.filter-row::before {
left: 0;
background: linear-gradient(90deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
}
.filter-row::after {
right: 0;
background: linear-gradient(270deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
}
body.dark-theme .filter-row::before,
body.dark-theme .filter-row::after {
background: linear-gradient(90deg, rgba(30,30,30,0.8) 0%, rgba(30,30,30,0) 100%);
}
/* 自定义滚动条样式 */
.filter-row::-webkit-scrollbar,
.secondary-filters-container::-webkit-scrollbar,
.search-results::-webkit-scrollbar {
height: 4px; /* 更窄的滑块高度 */
width: 2px; /* 更窄的滑块宽度 */
}
.filter-row::-webkit-scrollbar-thumb,
.secondary-filters-container::-webkit-scrollbar-thumb,
.search-results::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 2px; /* 更小的圆角 */
border: none;
}
.filter-row::-webkit-scrollbar-track,
.secondary-filters-container::-webkit-scrollbar-track,
.search-results::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
.filter-btn {
padding: 8px 16px;
border-radius: 20px;
background-color: #e9ecef;
border: none;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
display: flex;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
}
.filter-btn:hover {
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.filter-btn.active {
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* 一级标签样式 */
.filter-btn[data-level="1"] {
font-size: 13px;
padding: 4px 12px;
}
/* 二级标签样式 */
.filter-btn[data-level="2"] {
font-size: 13px;
padding: 4px 12px;
}
/* 分类选择器颜色 */
.filter-btn.dev {
background-color: var(--dev-color, #4cc9f0) !important;
}
.filter-btn.edu {
background-color: var(--edu-color, #f72585) !important;
}
.filter-btn.tool {
background-color: var(--tool-color, #7209b7) !important;
}
.filter-btn.law {
background-color: var(--law-color, #4895ef) !important;
}
.filter-btn.ai {
background-color: var(--ai-color, #f8961e) !important;
}
.caret {
margin-right: 8px;
transition: transform 0.2s;
}
.caret.down {
transform: rotate(90deg);
}
/* 修改后的二级筛选容器样式 */
.secondary-filters-container {
width: 100%;
display: none;
flex-wrap: nowrap;
gap: 10px;
overflow-x: auto;
padding-bottom: 10px;
scrollbar-width: thin;
scrollbar-color: #c2c3c9d6;
justify-content: center;
padding: 0 15px;
scroll-behavior: smooth;
position: relative;
margin-left: -10px;
margin-right: -10px;
width: calc(100% + 20px);
}
/* 私有主分类 */
.category-title .badge {
font-size: 12px;
padding: 3px 6px;
vertical-align: middle;
margin-left: 8px;
}
/* 当内容超出时靠左 */
.secondary-filters-container.scrollable {
justify-content: flex-start;
}
/* 新增的滚动指示器 */
.secondary-filters-container::before,
.secondary-filters-container::after {
content: '';
position: sticky;
top: 0;
width: 40px;
height: 100%;
z-index: 1;
pointer-events: none;
}
.secondary-filters-container::before {
left: 0;
background: linear-gradient(90deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
}
.secondary-filters-container::after {
right: 0;
background: linear-gradient(270deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
}
body.dark-theme .secondary-filters-container::before,
body.dark-theme .secondary-filters-container::after {
background: linear-gradient(90deg, rgba(30,30,30,0.8) 0%, rgba(30,30,30,0) 100%);
}
/* 自定义二级筛选容器的滚动条样式 */
.secondary-filters-container::-webkit-scrollbar {
height: 6px;
}
.secondary-filters-container::-webkit-scrollbar-track {
background: transparent;
}
.secondary-filters-container::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 3px;
}
.secondary-filters-container.show {
display: flex;
}
.secondary-filters-wrapper {
width: 100%;
position: relative;
min-height: 30px;
}
.app-list-container {
margin-top: 20px;
position: relative;
width: 100%;
}
.category-title {
font-size: 20px;
font-weight: bold;
margin: 30px 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid #e9ecef70;
display: flex;
align-items: center;
transition: var(--title-transition);
}
.category-title i {
margin-right: 10px;
}
.app-group {
margin-bottom: 30px;
width: 100%;
}
.app-list {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
width: 100%;
}
/* 新增卡片动画样式 */
.app-item {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.app-item.visible {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 1200px) {
.app-list {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 992px) {
.app-list {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.app-list {
grid-template-columns: repeat(2, 1fr);
}
body {
padding: 15px;
}
.filter-container {
margin-left: -15px;
margin-right: -15px;
padding-left: 20px;
padding-right: 20px;
width: calc(100% + 30px);
}
.floating-btn::after {
display: none;
}
}
@media (max-width: 576px) {
.app-list {
grid-template-columns: 1fr;
}
body {
padding: 10px;
}
.filter-container {
margin-left: -10px;
margin-right: -10px;
padding-left: 15px;
padding-right: 15px;
width: calc(100% + 20px);
}
}
.app-item {
background-color: #ffffff69;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgb(0 0 0 / 13%);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
border: 1px solid #e9ecef;
text-decoration: none;
color: inherit;
position: relative;
width: 100%;
}
.app-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
border-color: var(--primary-color);
}
.app-icon {
width: 40px;
height: 40px;
margin-right: 15px;
border-radius: 8px;
object-fit: cover;
background-color: #f1f3f5;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: var(--primary-color);
transition: background-color 0.15s ease-in-out;
overflow: hidden;
}
.app-icon img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
margin: 0 !important;
}
.app-info {
flex: 1;
}
.app-title {
font-weight: 600;
margin-bottom: 4px;
transition: var(--title-transition);
}
.app-url {
font-size: 13px;
color: #6c757d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.15s ease-in-out;
}
.app-tags {
display: flex;
gap: 5px;
margin-top: 5px;
flex-wrap: wrap;
}
.app-tag {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
border-radius: 10px;
color: white;
}
/* 分类标签颜色 */
.tag-dev {
background-color: var(--dev-color, #4cc9f0) !important;
}
.tag-edu {
background-color: var(--edu-color, #f72585) !important;
}
.tag-tool {
background-color: var(--tool-color, #7209b7) !important;
}
.tag-law {
background-color: var(--law-color, #4895ef) !important;
}
.tag-ai {
background-color: var(--ai-color, #f8961e) !important;
}
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: 6c757d;
}
.app-group.hidden {
display: none;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(0,0,0,.1);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 修改后的聊天气泡提示框样式 */
.app-description {
position: absolute;
bottom: calc(100% + 10px);
left: 20px;
transform: none;
background-color: var(--tooltip-bg);
color: var(--tooltip-text);
padding: 10px 15px;
border-radius: 4px;
font-size: 14px;
max-width: 300px;
width: max-content;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 100;
text-align: left;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
/* 修改后的聊天气泡小三角 - 移到左侧 */
.app-description::after {
content: '';
position: absolute;
top: 100%;
left: 20px;
transform: none;
border-width: 8px;
border-style: solid;
border-color: var(--tooltip-bg) transparent transparent transparent;
}
.app-item:hover .app-description {
opacity: 1;
}
.footer {
margin-top: 40px;
padding: 20px 0;
border-top: 1px solid #eee;
text-align: center;
color: #777;
width: 102%;
margin-left: -13px;
margin-right: -20px;
box-sizing: border-box;
}
</style>
<style>
/* 右键菜单样式 */
.context-menu {
position: absolute;
z-index: 1000;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: none;
}
.context-menu.dark {
background-color: #333;
border-color: #555;
}
.context-menu-item {
padding: 8px 16px;
cursor: pointer;
white-space: nowrap;
}
.context-menu-item:hover {
background-color: #f0f0f0;
}
.context-menu-item.dark:hover {
background-color: #444;
}
.context-menu-divider {
height: 1px;
background-color: #eee;
margin: 4px 0;
}
.context-menu-divider.dark {
background-color: #555;
}
</style>
</head>
<body>
<!-- 背景图片容器 -->
<div class="bg-container bg-light"></div>
<div class="bg-container bg-dark"></div>
<div class="nav-header">
<h1>
{% if settings and settings.show_logo %}
{% if settings.logo_type == 'icon' %}
<i class="fas {{ settings.logo_icon }}"></i>
{% else %}
<img src="{{ settings.logo_image if settings.logo_image else '/static/favicon.png' }}" style="height: 1em; vertical-align: middle;">
{% endif %}
{% endif %}
{{ settings.site_title if settings else "应用导航中心" }}
</h1>
</div>
<div class="search-container">
<input type="text" class="search-input" placeholder="搜索应用、网址或分类..." id="searchInput">
<div class="search-results" id="searchResults"></div>
</div>
<div class="filter-container">
<div class="filter-row" id="primaryFilters">
<button class="filter-btn active" data-level="1" data-filter="all">
<span id="loadingSpinner" class="loading-spinner"></span>
加载中...
</button>
<!-- 一级标签按钮将通过JS动态生成 -->
</div>
<div class="secondary-filters-wrapper" id="secondaryFiltersWrapper">
<!-- 二级标签容器将通过JS动态生成 -->
</div>
</div>
<div class="app-list-container" id="appListContainer">
<div class="text-center py-5">
<div class="loading-spinner" style="width: 40px; height: 40px; margin: 0 auto 15px;"></div>
<p>正在加载应用数据...</p>
</div>
</div>
<div class="floating-buttons">
<button class="floating-btn" id="backToTopBtn" title="返回顶部" style="display: none;"></button>
<button class="floating-btn" id="themeToggle" title="切换主题">🌙</button>
<button class="floating-btn" id="compactToggle" title="简洁模式">📱</button>
<a href="/manage" class="floating-btn" id="adminBtn" title="后台管理"><i class="fas fa-cog" style="color: #6c757d;"></i></a>
<a href="/app/add" class="floating-btn" id="addBtn" title="添加应用"><i class="fas fa-plus" style="color: #28a745;"></i></a>
<a href="/login?next=/" class="floating-btn" id="loginBtn" title="登录/退出"><i class="fas fa-sign-in-alt" style="color: #007bff;"></i></a>
</div>
<!-- 右键菜单 -->
<div id="contextMenu" class="context-menu">
<div class="context-menu-item" data-action="open">打开</div>
<div class="context-menu-item" data-action="copy">复制地址</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" data-action="edit" style="display: none;">编辑</div>
<div class="context-menu-item" data-action="delete" style="display: none;">删除</div>
</div>
<footer class="footer">
{% include 'footer.html' %}
</footer>
<script>
// 全局变量存储应用和分类数据
let apps = [];
let categories = {};
let isLoggedIn = false;
// DOM元素
const appListContainer = document.getElementById('appListContainer');
const primaryFilters = document.getElementById('primaryFilters');
const secondaryFiltersWrapper = document.getElementById('secondaryFiltersWrapper');
const loadingSpinner = document.getElementById('loadingSpinner');
const loginBtn = document.getElementById('loginBtn');
const adminBtn = document.getElementById('adminBtn');
const backToTopBtn = document.getElementById('backToTopBtn');
let currentPrimaryFilter = 'all';
let currentSecondaryFilter = 'all';
let currentExpandedPrimary = null;
let activeSecondaryFilter = null; // 新增:当前激活的二级筛选
// 从后端加载数据
async function loadData() {
try {
// 检查登录状态
const loginCheck = await fetch('/api/check_login');
const loginData = await loginCheck.json();
isLoggedIn = loginData.logged_in;
// 更新登录按钮状态
updateLoginButton();
// 加载应用数据
const appsResponse = await fetch('/api/apps');
apps = await appsResponse.json();
// 加载分类数据
const categoriesResponse = await fetch('/api/categories');
categories = await categoriesResponse.json();
// 动态添加CSS变量
const style = document.createElement('style');
let cssVariables = '';
Object.entries(categories).forEach(([catId, catData]) => {
// 为主分类添加颜色变量
cssVariables += `:root { --${catId}-color: ${catData.color}; }\n`;
// 为子分类添加颜色变量
if (catData.sub) {
Object.entries(catData.sub).forEach(([subId, subData]) => {
if (subData.color) {
cssVariables += `:root { --${subId}-color: ${subData.color}; }\n`;
}
});
}
});
style.textContent = cssVariables;
document.head.appendChild(style);
// 转换应用数据结构添加tags数组
apps = apps.map(app => {
const mainCat = app.category.main;
const subCat = app.category.sub;
const mainCatName = categories[mainCat]?.name || mainCat;
const subCatName = categories[mainCat]?.sub[subCat]?.name || subCat;
const subCatColor = categories[mainCat]?.sub[subCat]?.color || categories[mainCat]?.color;
return {
...app,
tags: [
{
level: 1,
id: mainCat,
name: mainCatName,
color: categories[mainCat]?.color
},
{
level: 2,
id: subCat,
name: subCatName,
color: subCatColor
}
]
};
});
// 过滤掉私有分类的应用
if (!isLoggedIn) {
apps = apps.filter(app => {
const mainCat = app.category.main;
const subCat = app.category.sub;
// 检查主分类是否私有
const isMainPrivate = categories[mainCat]?.private || false;
// 检查子分类是否私有
const isSubPrivate = categories[mainCat]?.sub_private?.[subCat] || false;
return !isMainPrivate && !isSubPrivate;
});
// 过滤掉私有分类
Object.keys(categories).forEach(catId => {
if (categories[catId].private) {
delete categories[catId];
} else if (categories[catId].sub_private) {
Object.keys(categories[catId].sub_private).forEach(subId => {
if (categories[catId].sub_private[subId]) {
delete categories[catId].sub[subId];
}
});
}
});
}
// 按权重对分类进行排序(无论是否登录都排序)
categories = Object.fromEntries(
Object.entries(categories).sort((a, b) => {
const weightA = a[1].weight || 0;
const weightB = b[1].weight || 0;
return weightB - weightA; // 降序排列
})
);
// 对每个主分类下的子分类也按权重排序
Object.values(categories).forEach(cat => {
if (cat.sub) {
cat.sub = Object.fromEntries(
Object.entries(cat.sub).sort((a, b) => {
const weightA = a[1].weight || 0;
const weightB = b[1].weight || 0;
return weightB - weightA; // 降序排列
})
);
}
});
// 对每个主分类下的子分类也按权重排序
Object.values(categories).forEach(cat => {
if (cat.sub) {
cat.sub = Object.fromEntries(
Object.entries(cat.sub).sort((a, b) => {
const weightA = a[1].weight || 0;
const weightB = b[1].weight || 0;
return weightB - weightA; // 降序排列
})
);
}
});
// 渲染界面
renderFilters();
renderApps();
loadingSpinner.style.display = 'none';
primaryFilters.querySelector('[data-filter="all"]').textContent = '全部';
} catch (error) {
console.error('加载数据失败:', error);
appListContainer.innerHTML = `
<div class="alert alert-danger">
加载数据失败,请刷新页面重试
</div>
`;
}
}
// 更新登录按钮状态
function updateLoginButton() {
if (isLoggedIn) {
loginBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
loginBtn.href = '/logout';
loginBtn.title = '退出登录';
adminBtn.href = '/manage'; // 已登录时直接跳转到管理页面
document.getElementById('addBtn').style.display = 'flex'; // 显示添加按钮
} else {
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i>';
loginBtn.href = '/login';
loginBtn.title = '登录';
adminBtn.href = '/login?next=/manage'; // 未登录时跳转到登录页面
document.getElementById('addBtn').style.display = 'none'; // 隐藏添加按钮
}
}
// 根据分类ID获取对应的颜色类
function getColorForCategory(categoryId) {
return categoryId;
}
// 生成筛选按钮
function renderFilters() {
// 生成一级标签
primaryFilters.innerHTML = '';
const allPrimaryBtn = document.createElement('button');
allPrimaryBtn.className = 'filter-btn active';
allPrimaryBtn.textContent = '全部';
allPrimaryBtn.dataset.level = '1';
allPrimaryBtn.dataset.filter = 'all';
allPrimaryBtn.addEventListener('click', () => {
setFilter('all', 'all');
showAllSecondaryFilters();
setAllPrimaryCaretDown(true);
activeSecondaryFilter = null; // 重置二级筛选状态
});
primaryFilters.appendChild(allPrimaryBtn);
// 添加一级标签按钮(从分类数据生成)
Object.entries(categories).forEach(([catId, catData]) => {
const btn = document.createElement('button');
btn.className = `filter-btn ${catId}`;
btn.innerHTML = `<i class="fas fa-caret-right caret down"></i> ${catData.name}`;
btn.dataset.level = '1';
btn.dataset.filter = catId;
// 确保有颜色时才设置背景色
if (catData.color) {
btn.style.backgroundColor = catData.color;
btn.style.color = getContrastColor(catData.color);
}
btn.addEventListener('click', (e) => {
if (currentExpandedPrimary === catId) {
// 如果点击的是已展开的一级分类,则显示所有二级分类
showAllSecondaryFilters();
setFilter('all', 'all');
setAllPrimaryCaretDown(true);
activeSecondaryFilter = null; // 重置二级筛选状态
} else {
// 否则展开该一级分类下的二级分类
expandSecondary(catId);
setFilter(catId, 'all');
setAllPrimaryCaretDown(false);
setPrimaryCaretDown(catId, true);
activeSecondaryFilter = null; // 重置二级筛选状态
}
});
primaryFilters.appendChild(btn);
});
// 初始加载时显示所有二级标签
showAllSecondaryFilters();
}
// 设置所有一级标签箭头的展开/收起状态
function setAllPrimaryCaretDown(shouldDown) {
const primaryBtns = primaryFilters.querySelectorAll('[data-level="1"]');
primaryBtns.forEach(btn => {
if (btn.dataset.filter !== 'all') {
const caret = btn.querySelector('.caret');
if (caret) {
if (shouldDown) {
caret.classList.add('down');
} else {
caret.classList.remove('down');
}
}
}
});
}
// 设置特定一级标签箭头的展开/收起状态
function setPrimaryCaretDown(primaryId, shouldDown) {
const btn = primaryFilters.querySelector(`[data-filter="${primaryId}"]`);
if (btn) {
const caret = btn.querySelector('.caret');
if (caret) {
if (shouldDown) {
caret.classList.add('down');
} else {
caret.classList.remove('down');
}
}
}
}
// 显示所有二级标签
function showAllSecondaryFilters() {
collapseAllSecondary();
const container = document.createElement('div');
container.className = 'secondary-filters-container show';
container.id = 'secondary-all';
const allSecondaryBtn = document.createElement('button');
allSecondaryBtn.className = 'filter-btn active';
allSecondaryBtn.textContent = '全部';
allSecondaryBtn.dataset.level = '2';
allSecondaryBtn.dataset.filter = 'all';
allSecondaryBtn.addEventListener('click', () => {
setFilter('all', 'all');
activeSecondaryFilter = null; // 重置二级筛选状态
});
container.appendChild(allSecondaryBtn);
// 添加所有二级标签按钮(从分类数据生成)
Object.entries(categories).forEach(([mainCatId, mainCatData]) => {
Object.entries(mainCatData.sub).forEach(([subCatId, subCatData]) => {
const btn = document.createElement('button');
btn.className = `filter-btn ${mainCatId}`;
btn.textContent = subCatData.name;
btn.dataset.level = '2';
btn.dataset.filter = subCatId;
btn.style.backgroundColor = subCatData.color;
btn.style.color = getContrastColor(subCatData.color);
btn.addEventListener('click', () => {
// 如果点击的是当前激活的二级筛选,则取消筛选
if (activeSecondaryFilter === subCatId) {
setFilter('all', 'all');
activeSecondaryFilter = null;
} else {
setFilter('all', subCatId);
activeSecondaryFilter = subCatId;
}
});
container.appendChild(btn);
});
});
secondaryFiltersWrapper.innerHTML = '';
secondaryFiltersWrapper.appendChild(container);
currentExpandedPrimary = null;
// 为新创建的二级筛选容器添加滚轮事件
addWheelEventToContainer(container);
}
// 展开二级标签
function expandSecondary(primaryFilterId) {
collapseAllSecondary();
const primaryBtn = primaryFilters.querySelector(`[data-filter="${primaryFilterId}"]`);
if (primaryBtn && categories[primaryFilterId]) {
const container = document.createElement('div');
container.className = 'secondary-filters-container show';
container.id = `secondary-${primaryFilterId}`;
const allSecondaryBtn = document.createElement('button');
allSecondaryBtn.className = 'filter-btn active';
allSecondaryBtn.textContent = '全部';
allSecondaryBtn.dataset.level = '2';
allSecondaryBtn.dataset.filter = 'all';
allSecondaryBtn.addEventListener('click', () => {
setFilter(primaryFilterId, 'all');
activeSecondaryFilter = null; // 重置二级筛选状态
});
container.appendChild(allSecondaryBtn);
Object.entries(categories[primaryFilterId].sub).forEach(([subCatId, subCatData]) => {
const btn = document.createElement('button');
btn.className = `filter-btn ${primaryFilterId}`;
btn.textContent = subCatData.name;
btn.dataset.level = '2';
btn.dataset.filter = subCatId;
btn.style.backgroundColor = subCatData.color;
btn.style.color = getContrastColor(subCatData.color);
btn.addEventListener('click', () => {
// 如果点击的是当前激活的二级筛选,则取消筛选
if (activeSecondaryFilter === subCatId) {
setFilter(primaryFilterId, 'all');
activeSecondaryFilter = null;
} else {
setFilter(primaryFilterId, subCatId);
activeSecondaryFilter = subCatId;
}
});
container.appendChild(btn);
});
secondaryFiltersWrapper.innerHTML = '';
secondaryFiltersWrapper.appendChild(container);
currentExpandedPrimary = primaryFilterId;
// 为新创建的二级筛选容器添加滚轮事件
addWheelEventToContainer(container);
}
}
// 收起所有二级标签
function collapseAllSecondary() {
secondaryFiltersWrapper.innerHTML = '';
currentExpandedPrimary = null;
}
// 为滚动容器添加滚轮事件
function addWheelEventToContainer(container) {
container.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
container.scrollLeft += e.deltaY;
}
});
}
// 设置当前筛选条件
function setFilter(primaryFilter, secondaryFilter) {
currentPrimaryFilter = primaryFilter;
currentSecondaryFilter = secondaryFilter;
// 更新按钮激活状态
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
// 激活一级标签按钮
if (primaryFilter === 'all') {
primaryFilters.querySelector('[data-filter="all"]').classList.add('active');
} else {
const primaryBtn = primaryFilters.querySelector(`[data-filter="${primaryFilter}"]`);
if (primaryBtn) primaryBtn.classList.add('active');
}
// 激活二级标签按钮
if (secondaryFilter === 'all') {
const container = document.querySelector('.secondary-filters-container');
if (container) {
const allBtn = container.querySelector('[data-filter="all"]');
if (allBtn) allBtn.classList.add('active');
}
} else {
const secondaryBtn = document.querySelector(`[data-filter="${secondaryFilter}"]`);
if (secondaryBtn) secondaryBtn.classList.add('active');
}
// 重新渲染应用列表
renderApps();
}
// 渲染应用列表
function renderApps() {
appListContainer.innerHTML = '';
// 按一级标签分组
Object.entries(categories).forEach(([mainCatId, mainCatData]) => {
const groupApps = apps.filter(app => app.category.main === mainCatId);
// 应用筛选条件
let filteredGroupApps = groupApps;
// 一级标签筛选
if (currentPrimaryFilter !== 'all' && currentPrimaryFilter !== mainCatId) {
return; // 跳过不匹配的一级标签组
}
// 二级标签筛选
if (currentSecondaryFilter !== 'all') {
filteredGroupApps = filteredGroupApps.filter(app =>
app.category.sub === currentSecondaryFilter
);
if (filteredGroupApps.length === 0) {
return;
}
}
// 对应用按权重排序(权重越大越靠前)
filteredGroupApps.sort((a, b) => {
const weightA = a.weight || 0;
const weightB = b.weight || 0;
return weightB - weightA; // 降序排列
});
// 获取该分类下第一个应用的图标(如果有应用的话)
const firstAppIcon = filteredGroupApps.length > 0 ?
filteredGroupApps[0].icon :
'fa-cube'; // 默认图标
// 创建分类组
const groupDiv = document.createElement('div');
groupDiv.className = 'app-group';
// 检查该主分类是否为私有分类且用户已登录
const isPrivateCategory = mainCatData.private && isLoggedIn;
// 添加分类标题(使用第一个应用的图标)
const titleDiv = document.createElement('div');
titleDiv.className = 'category-title';
titleDiv.innerHTML = `<i class="fas ${firstAppIcon}"></i> ${mainCatData.name}`;
// 如果是私有分类且已登录,添加私有标记
if (isPrivateCategory) {
const privateBadge = document.createElement('span');
privateBadge.className = 'badge bg-warning text-dark ms-2';
privateBadge.textContent = '私有';
titleDiv.appendChild(privateBadge);
}
groupDiv.appendChild(titleDiv);
// 创建应用列表
const appList = document.createElement('div');
appList.className = 'app-list';
// 添加应用卡片
filteredGroupApps.forEach(app => {
const subCatName = categories[app.category.main]?.sub[app.category.sub]?.name || app.category.sub;
const subCatColor = categories[app.category.main]?.sub[app.category.sub]?.color || mainCatData.color;
const appItem = document.createElement('a');
appItem.className = `app-item ${settings.card_style === 'compact' ? 'compact' : ''}`;
appItem.href = app.url;
appItem.target = '_blank';
// 如果是私有应用且已登录,添加私有标记
if (app.private && isLoggedIn) {
const privateBadge = document.createElement('div');
privateBadge.className = 'private-badge';
appItem.appendChild(privateBadge);
}
// 添加聊天气泡描述提示框
const descriptionDiv = document.createElement('div');
descriptionDiv.className = 'app-description';
descriptionDiv.textContent = app.description || '暂无描述';
// 判断是Font Awesome图标还是自定义图片
const iconHtml = app.icon.startsWith('/') || app.icon.startsWith('http') ?
`<img src="${app.icon}" alt="图标">` :
`<i class="fas ${app.icon}"></i>`;
appItem.innerHTML += `
<div class="app-icon">
${iconHtml}
</div>
<div class="app-info">
<div class="app-title">${app.title}</div>
<div class="app-url">${new URL(app.url).hostname}</div>
<div class="app-tags">
<span class="app-tag" style="background-color: ${subCatColor}; color: ${getContrastColor(subCatColor)}">${subCatName}</span>
</div>
</div>
`;
appItem.prepend(descriptionDiv);
appList.appendChild(appItem);
});
groupDiv.appendChild(appList);
appListContainer.appendChild(groupDiv);
});
// 如果没有显示任何分类组,显示无结果提示
if (appListContainer.children.length === 0) {
const noResults = document.createElement('div');
noResults.className = 'no-results';
noResults.textContent = '没有找到匹配的应用';
appListContainer.appendChild(noResults);
}
// 添加卡片动画效果
animateCards();
}
// 添加卡片动画效果
function animateCards() {
const cards = document.querySelectorAll('.app-item');
cards.forEach((card, index) => {
// 使用setTimeout实现卡片依次出现的效果
setTimeout(() => {
card.classList.add('visible');
}, index * 10); // 每个卡片间隔50ms
});
}
// 辅助函数:计算对比色
function getContrastColor(hexColor) {
if (!hexColor) return '#ffffff';
// 转换hex颜色为RGB
const r = parseInt(hexColor.substr(1, 2), 16);
const g = parseInt(hexColor.substr(3, 2), 16);
const b = parseInt(hexColor.substr(5, 2), 16);
// 计算亮度
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// 根据亮度返回黑色或白色
return brightness > 128 ? '#000000' : '#ffffff';
}
// 全局设置
let settings = {
theme: 'auto',
card_style: 'normal',
search_history: []
};
// 加载设置
async function loadSettings() {
try {
// 检查是否登录
isLoggedIn = await checkLoginStatus();
// 获取游客默认背景设置
const guestResponse = await fetch('/api/guest_settings');
const guestSettings = await guestResponse.json();
if (isLoggedIn) {
// 如果已登录,从服务器获取系统设置
const response = await fetch('/api/settings');
settings = await response.json();
// 设置背景图片
const lightBg = settings.bg_image === 'none' ? 'none' :
(settings.bg_image || '/static/background_light.jpg');
const darkBg = settings.dark_bg_image === 'none' ? 'none' :
(settings.dark_bg_image || '/static/background_dark.jpg');
setBackgroundImages(lightBg, darkBg);
} else {
// 如果未登录,先检查本地存储
const localSettings = localStorage.getItem('navSettings');
if (localSettings) {
const localSettingsObj = JSON.parse(localSettings);
// 从本地存储加载主题和卡片样式
settings.theme = localSettingsObj.theme || guestSettings.theme;
settings.card_style = localSettingsObj.card_style || guestSettings.card_style;
settings.search_history = localSettingsObj.search_history || [];
} else {
// 首次访问,使用接口返回的默认配置
settings.theme = guestSettings.theme;
settings.card_style = guestSettings.card_style;
}
// 设置背景图片(始终使用服务器配置)
const lightBg = guestSettings.bg_image === 'none' ? 'none' :
(guestSettings.bg_image || '/static/background_light.jpg');
const darkBg = guestSettings.dark_bg_image === 'none' ? 'none' :
(guestSettings.dark_bg_image || '/static/background_dark.jpg');
setBackgroundImages(lightBg, darkBg);
}
applySettings();
} catch (error) {
console.error('加载设置失败:', error);
}
}
// 设置背景图片
function setBackgroundImages(lightImage, darkImage) {
// 移除现有的视频背景
document.querySelectorAll('.video-bg-container').forEach(el => el.remove());
// 处理明亮模式背景
if (lightImage && lightImage.endsWith('.mp4')) {
const lightVideoContainer = document.createElement('div');
lightVideoContainer.className = 'video-bg-container bg-light';
lightVideoContainer.style.display = document.body.classList.contains('dark-theme') ? 'none' : 'block';
const lightVideo = document.createElement('video');
lightVideo.className = 'video-bg';
lightVideo.src = lightImage;
lightVideo.autoplay = true;
lightVideo.loop = true;
lightVideo.muted = true;
lightVideo.playsInline = true;
lightVideoContainer.appendChild(lightVideo);
document.body.appendChild(lightVideoContainer);
} else {
document.documentElement.style.setProperty('--bg-image', lightImage === 'none' ? 'none' : `url('${lightImage}')`);
}
// 处理暗黑模式背景
if (darkImage && darkImage.endsWith('.mp4')) {
const darkVideoContainer = document.createElement('div');
darkVideoContainer.className = 'video-bg-container bg-dark';
darkVideoContainer.style.display = document.body.classList.contains('dark-theme') ? 'block' : 'none';
const darkVideo = document.createElement('video');
darkVideo.className = 'video-bg';
darkVideo.src = darkImage;
darkVideo.autoplay = true;
darkVideo.loop = true;
darkVideo.muted = true;
darkVideo.playsInline = true;
darkVideoContainer.appendChild(darkVideo);
document.body.appendChild(darkVideoContainer);
} else {
document.documentElement.style.setProperty('--dark-bg-image', darkImage === 'none' ? 'none' : `url('${darkImage}')`);
}
// 添加背景加载完成的类
setTimeout(() => {
document.body.classList.add('bg-loaded');
}, 100);
}
// 检查登录状态
async function checkLoginStatus() {
try {
const response = await fetch('/api/check_login');
const data = await response.json();
return data.logged_in;
} catch (error) {
console.error('检查登录状态失败:', error);
return false;
}
}
// 应用设置
function applySettings() {
// 应用主题
document.body.classList.remove('dark-theme', 'light-theme');
if (settings.theme === 'dark' ||
(settings.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.body.classList.add('dark-theme');
// 显示/隐藏视频背景
document.querySelectorAll('.video-bg-container.bg-light').forEach(el => el.style.display = 'none');
document.querySelectorAll('.video-bg-container.bg-dark').forEach(el => el.style.display = 'block');
} else if (settings.theme === 'light') {
document.body.classList.add('light-theme');
// 显示/隐藏视频背景
document.querySelectorAll('.video-bg-container.bg-light').forEach(el => el.style.display = 'block');
document.querySelectorAll('.video-bg-container.bg-dark').forEach(el => el.style.display = 'none');
}
// 应用卡片样式
document.querySelectorAll('.app-item').forEach(item => {
item.classList.toggle('compact', settings.card_style === 'compact');
});
// 更新主题按钮状态
updateThemeButtonIcon();
}
// 更新主题按钮图标
function updateThemeButtonIcon() {
const themeToggle = document.getElementById('themeToggle');
if (document.body.classList.contains('dark-theme')) {
themeToggle.textContent = '☀️';
themeToggle.title = '切换至明亮模式';
} else {
themeToggle.textContent = '🌙';
themeToggle.title = '切换至暗黑模式';
}
}
// 保存设置
function saveSettings() {
if (isLoggedIn) {
fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings)
});
} else {
// 游客只保存主题和卡片样式到本地
const settingsToSave = {
theme: settings.theme,
card_style: settings.card_style,
search_history: settings.search_history
};
localStorage.setItem('navSettings', JSON.stringify(settingsToSave));
}
}
// 搜索功能
function setupSearch() {
const searchInput = document.getElementById('searchInput');
const searchResults = document.getElementById('searchResults');
let searchTimeout;
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const keyword = searchInput.value.trim();
if (keyword.length > 0) {
fetch('/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ keyword })
})
.then(response => response.json())
.then(results => {
searchResults.innerHTML = '';
if (results.length > 0) {
results.forEach(app => {
const item = document.createElement('div');
item.className = 'search-result-item';
item.innerHTML = `
<div><strong>${app.title}</strong></div>
<div style="font-size:12px;color:#666;">${app.url}</div>
`;
item.addEventListener('click', () => {
window.open(app.url, '_blank');
});
searchResults.appendChild(item);
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = '<div class="search-result-item">无搜索结果</div>';
searchResults.style.display = 'block';
}
});
} else {
searchResults.style.display = 'none';
}
}, 300);
});
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
}
// 主题切换功能
function setupThemeSwitcher() {
const themeToggle = document.getElementById('themeToggle');
themeToggle.addEventListener('click', () => {
// 使用requestAnimationFrame确保在浏览器重绘前完成所有样式变更
requestAnimationFrame(() => {
// 确定新主题
const newTheme = document.body.classList.contains('dark-theme') ? 'light' : 'dark';
// 更新界面主题
document.body.classList.remove('dark-theme', 'light-theme');
if (newTheme === 'dark') {
document.body.classList.add('dark-theme');
// 更新视频背景显示状态
document.querySelectorAll('.video-bg-container.bg-light').forEach(el => el.style.display = 'none');
document.querySelectorAll('.video-bg-container.bg-dark').forEach(el => el.style.display = 'block');
} else {
document.body.classList.add('light-theme');
// 更新视频背景显示状态
document.querySelectorAll('.video-bg-container.bg-light').forEach(el => el.style.display = 'block');
document.querySelectorAll('.video-bg-container.bg-dark').forEach(el => el.style.display = 'none');
}
// 更新主题按钮图标
updateThemeButtonIcon();
// 更新本地设置(仅主题)
settings.theme = newTheme;
// 保存设置(不会影响后台的背景设置)
if (isLoggedIn) {
// 已登录用户只更新主题设置
fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ theme: newTheme })
}).catch(error => {
console.error('保存主题设置失败:', error);
});
} else {
// 游客保存到本地(仅保存主题、卡片样式和搜索历史)
const settingsToSave = {
theme: newTheme,
card_style: settings.card_style,
search_history: settings.search_history
};
localStorage.setItem('navSettings', JSON.stringify(settingsToSave));
}
});
});
}
// 简洁模式切换
function setupCompactToggle() {
const compactToggle = document.getElementById('compactToggle');
compactToggle.addEventListener('click', () => {
settings.card_style = settings.card_style === 'compact' ? 'normal' : 'compact';
applySettings();
saveSettings();
});
}
// 后台管理按钮点击事件
function setupAdminButton() {
adminBtn.addEventListener('click', async (e) => {
if (!isLoggedIn) {
e.preventDefault();
const loginCheck = await fetch('/api/check_login');
const loginData = await loginCheck.json();
if (!loginData.logged_in) {
window.location.href = '/login?next=/manage';
} else {
window.location.href = '/manage';
}
}
});
}
// 返回顶部功能
function setupBackToTop() {
// 监听滚动事件
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
backToTopBtn.style.display = 'flex';
} else {
backToTopBtn.style.display = 'none';
}
});
// 点击返回顶部按钮
backToTopBtn.addEventListener('click', () => {
// 平滑滚动到顶部
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// 右键菜单功能
function setupContextMenu() {
const contextMenu = document.getElementById('contextMenu');
let currentApp = null;
let currentAppIndex = -1;
// 显示右键菜单
document.addEventListener('contextmenu', (e) => {
// 检查是否点击的是应用卡片
const appItem = e.target.closest('.app-item');
if (appItem) {
e.preventDefault();
// 找到对应的应用数据
const appTitle = appItem.querySelector('.app-title').textContent;
currentApp = apps.find(app => app.title === appTitle);
currentAppIndex = apps.findIndex(app => app.title === appTitle);
// 更新菜单位置
contextMenu.style.left = `${e.pageX}px`;
contextMenu.style.top = `${e.pageY}px`;
contextMenu.style.display = 'block';
// 根据主题设置菜单样式
if (document.body.classList.contains('dark-theme')) {
contextMenu.classList.add('dark');
contextMenu.querySelectorAll('.context-menu-item').forEach(item => item.classList.add('dark'));
contextMenu.querySelectorAll('.context-menu-divider').forEach(divider => divider.classList.add('dark'));
} else {
contextMenu.classList.remove('dark');
contextMenu.querySelectorAll('.context-menu-item').forEach(item => item.classList.remove('dark'));
contextMenu.querySelectorAll('.context-menu-divider').forEach(divider => divider.classList.remove('dark'));
}
// 显示/隐藏编辑和删除按钮(仅管理员可见)
const editItem = contextMenu.querySelector('[data-action="edit"]');
const deleteItem = contextMenu.querySelector('[data-action="delete"]');
if (isLoggedIn) {
editItem.style.display = 'block';
deleteItem.style.display = 'block';
} else {
editItem.style.display = 'none';
deleteItem.style.display = 'none';
}
}
});
// 隐藏右键菜单
document.addEventListener('click', (e) => {
if (!contextMenu.contains(e.target)) {
contextMenu.style.display = 'none';
}
});
// 菜单项点击处理
contextMenu.addEventListener('click', (e) => {
const menuItem = e.target.closest('.context-menu-item');
if (!menuItem || !currentApp) return;
const action = menuItem.dataset.action;
switch (action) {
case 'open':
window.open(currentApp.url, '_blank');
break;
case 'copy':
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(currentApp.url)
.then(() => {
showCopyNotification('已复制到剪贴板');
})
.catch(err => {
console.error('复制失败:', err);
fallbackCopyText(currentApp.url);
});
} else {
fallbackCopyText(currentApp.url);
}
break;
case 'edit':
if (isLoggedIn) {
window.location.href = `/app/edit/${currentAppIndex}`;
}
break;
case 'delete':
if (isLoggedIn) {
if (confirm(`确定要删除 "${currentApp.title}" 吗?`)) {
fetch(`/app/delete/${currentAppIndex}`, {
method: 'GET'
})
.then(response => {
if (response.ok) {
// 重新加载数据
loadData();
} else {
alert('删除失败');
}
})
.catch(error => {
console.error('删除失败:', error);
alert('删除失败');
});
}
}
break;
}
contextMenu.style.display = 'none';
});
}
// 在DOMContentLoaded事件中添加setupContextMenu调用
document.addEventListener('DOMContentLoaded', () => {
loadData();
loadSettings();
setupSearch();
setupThemeSwitcher();
setupCompactToggle();
setupAdminButton();
setupBackToTop();
setupContextMenu(); // 添加右键菜单功能
// 为静态的一级筛选容器添加滚轮事件
primaryFilters.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
primaryFilters.scrollLeft += e.deltaY;
}
});
});
// 显示复制成功的提示
function showCopyNotification(message) {
const notification = document.createElement('div');
notification.className = 'copy-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => notification.remove(), 300);
}, 1000);
}
// 备用的复制方法
function fallbackCopyText(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed'; // 防止页面滚动
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? '已复制到剪贴板' : '复制失败,请手动复制';
showCopyNotification(msg);
} catch (err) {
console.error('备用复制方法失败:', err);
showCopyNotification('复制失败,请手动复制');
}
document.body.removeChild(textarea);
}
function checkScrollable() {
document.querySelectorAll('.filter-row, .secondary-filters-container').forEach(container => {
// 检查内容宽度是否大于容器宽度
const isScrollable = container.scrollWidth > container.clientWidth;
if(isScrollable) {
container.classList.add('scrollable');
} else {
container.classList.remove('scrollable');
}
});
}
// 初始化时检查
window.addEventListener('load', checkScrollable);
// 窗口大小改变时检查
window.addEventListener('resize', checkScrollable);
// 内容变化时检查(如筛选器更新后)
new MutationObserver(checkScrollable).observe(document.body, {
childList: true,
subtree: true
});
</script>
</body>
</html>