AIDaohang/templates/index.html
2025-07-08 19:54:53 +08:00

1663 lines
50 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">
<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;
}
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.3s ease, color 0.3s ease;
}
/* 新增私有卡片标记样式 */
.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.5s ease;
}
/* 视频背景容器 */
.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;
}
/* 移除导航栏按钮样式 */
.nav-header {
margin-bottom: 20px;
}
/* 新增的搜索框样式 */
.search-container {
margin: 20px auto;
max-width: 600px;
position: relative;
}
.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.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.search-results {
position: absolute;
width: 100%;
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.3s ease;
}
.search-result-item {
padding: 10px 20px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.3s ease;
}
.search-result-item:hover {
background: #f5f5f5;
}
/* 右下角按钮组 */
.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: #eee;
}
/* 简洁模式卡片样式 */
.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.3s ease, color 0.3s ease;
}
/* 修改后的筛选行样式 - 支持横向滚动 */
.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 20px; /* 添加内边距 */
scroll-behavior: smooth;
position: relative;
}
/* 当内容超出时靠左 */
.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 {
height: 6px;
}
.filter-row::-webkit-scrollbar-track {
background: transparent;
}
.filter-row::-webkit-scrollbar-thumb {
background-color: var(--primary-color);
border-radius: 3px;
}
.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 20px; /* 添加内边距 */
scroll-behavior: smooth;
position: relative;
}
/* 私有主分类 */
.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;
}
.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: color 0.3s ease, border-color 0.3s ease;
}
.category-title i {
margin-right: 10px;
}
.app-group {
margin-bottom: 30px;
}
.app-list {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
@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);
}
}
@media (max-width: 576px) {
.app-list {
grid-template-columns: 1fr;
}
}
.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;
}
.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.3s ease;
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: color 0.3s ease;
}
.app-url {
font-size: 13px;
color: #6c757d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.3s ease;
}
.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;
}
</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>
</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">
<a href="/login?next=/" class="floating-btn" id="loginBtn" title="登录/退出"><i class="fas fa-sign-in-alt"></i></a>
<a href="/manage" class="floating-btn" id="adminBtn" title="后台管理"><i class="fas fa-cog"></i></a>
<button class="floating-btn" id="themeToggle" title="切换主题">🌙</button>
<button class="floating-btn" id="compactToggle" title="简洁模式">📱</button>
</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');
let currentPrimaryFilter = 'all';
let currentSecondaryFilter = 'all';
let currentExpandedPrimary = 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'; // 已登录时直接跳转到管理页面
} else {
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i>';
loginBtn.href = '/login';
loginBtn.title = '登录';
adminBtn.href = '/login?next=/manage'; // 未登录时跳转到登录页面
}
}
// 根据分类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);
});
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);
} else {
// 否则展开该一级分类下的二级分类
expandSecondary(catId);
setFilter(catId, 'all');
setAllPrimaryCaretDown(false);
setPrimaryCaretDown(catId, true);
}
});
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');
});
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', () => {
setFilter('all', 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');
});
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', () => {
setFilter(primaryFilterId, 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);
}
}
// 辅助函数:计算对比色
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', () => {
// 只在明亮和暗黑之间切换
if (document.body.classList.contains('dark-theme')) {
settings.theme = 'light';
document.body.classList.remove('dark-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');
} else {
settings.theme = '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');
}
updateThemeButtonIcon();
saveSettings();
});
}
// 简洁模式切换
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';
}
}
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
loadSettings();
setupSearch();
setupThemeSwitcher();
setupCompactToggle();
setupAdminButton();
// 为静态的一级筛选容器添加滚轮事件
primaryFilters.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
primaryFilters.scrollLeft += e.deltaY;
}
});
});
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>