变迁左右滑动、自定义页脚等功能,应用编辑图标BUG修复

This commit is contained in:
王志珏 2025-07-07 20:08:59 +08:00
parent ee114dda50
commit e9a755fbe1
10 changed files with 1531 additions and 1242 deletions

View File

@ -14,29 +14,28 @@
### 界面优化
1. 每行卡片数量支持自定义4个、5个、6个、8个
2. 应用的图标支持上传图片自定义 - 已完成
3. 首页增加页脚
4. 一级分类和二级分类固定宽度,超出宽度可左右滑动查看
3. 自定义页脚 - 已完成
4. 一级分类和二级分类固定宽度,超出宽度可左右滑动查看 - 已完成
5. 气泡的小箭头靠左对齐 - 已完成
6. logo图标设置区分明亮和暗黑模式
7. 私有应用在首页添加标识
8. 首页卡片右键菜单
9. 应用支持配置多个URL左键打开默认URL右键可选择URL进行复制地址或者打开或者编辑应用
10. 新增应用界面便捷增加分类
11. 新增应用界面便捷增加图标图片
### 功能增强
8. 应用管理页支持分页和按一级分类筛选
9. 应用分页和附件分页功能
10. 网站图标自动获取功能
11. 书签收藏工具
1. 应用管理页支持分页和按一级分类筛选 - 已完成
2. 应用分页和附件分页功能 - 已完成
3. 网站图标自动获取功能
4. 书签收藏工具
### BUG修复
12. 应用编辑页面没有回显带入图片
1. 应用编辑页面没有回显带入图片 - 已解决
### 批量操作
13. 应用批量选择功能:
1. 应用批量选择功能:
- 批量删除
- 批量设置私有化/公有化
14. 附件批量选择功能:
2. 附件批量选择功能:
- 批量删除
15. 新增应用界面便捷增加分类
16. 新增应用界面便捷增加图标图片

87
app.py
View File

@ -11,6 +11,7 @@ from functools import wraps
from werkzeug.utils import secure_filename
import requests
from urllib.parse import urlparse
from math import ceil
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # 请更改为安全的密钥
@ -44,6 +45,7 @@ SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
GUEST_SETTINGS_FILE = os.path.join(DATA_DIR, 'guest_settings.json')
def migrate_settings(settings):
"""迁移旧版设置到新版格式"""
if 'admin_password' in settings:
@ -59,6 +61,12 @@ def migrate_settings(settings):
'123456',
method='pbkdf2:sha256'
)
# 确保有页脚设置
if 'footer_html' not in settings:
settings[
'footer_html'] = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
return settings
@ -78,7 +86,8 @@ def init_settings():
"logo_icon": "fa-th-list",
"logo_image": "",
"uploaded_backgrounds": [],
"uploaded_logos": []
"uploaded_logos": [],
"footer_html": '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
}
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(default_settings, f, ensure_ascii=False, indent=2)
@ -90,6 +99,14 @@ def init_settings():
if 'admin_password_hash' not in settings:
# 需要迁移
settings = migrate_settings(settings)
# 添加新字段的默认值
if 'footer_html' not in settings:
settings['footer_html'] = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
elif 'footer_html' not in settings:
# 已有密码哈希但没有页脚设置的情况
settings['footer_html'] = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
@ -356,6 +373,7 @@ def system_settings():
settings['site_title'] = request.form.get('site_title', '应用导航中心')
settings['show_logo'] = 'show_logo' in request.form
settings['logo_type'] = request.form.get('logo_type', 'icon')
settings['footer_html'] = request.form.get('footer_html', '')
# 处理logo设置
if settings['logo_type'] == 'icon':
@ -467,9 +485,38 @@ def system_settings():
@app.route('/attachments')
@login_required
def manage_attachments():
type_filter = request.args.get('type', 'all')
search_query = request.args.get('search', '').lower()
page = int(request.args.get('page', 1))
per_page = 10 # 每页显示10条数据
settings = load_settings()
attachments = load_attachments()
return render_template('attachments.html', settings=settings, attachments=attachments)
# 应用筛选
filtered_attachments = []
for attachment in attachments:
# 类型筛选
if type_filter != 'all' and attachment['type'] != type_filter:
continue
# 搜索筛选
if search_query and search_query not in attachment['filename'].lower():
continue
filtered_attachments.append(attachment)
# 分页处理
total_pages = ceil(len(filtered_attachments) / per_page)
paginated_attachments = filtered_attachments[(page - 1) * per_page: page * per_page]
return render_template('attachments.html',
settings=settings,
attachments=paginated_attachments,
current_page=page,
total_pages=total_pages,
search_query=search_query,
type_filter=type_filter)
@app.route('/upload_attachment', methods=['POST'])
@ -573,21 +620,45 @@ def handle_settings():
@login_required
def index():
category_filter = request.args.get('category')
search_query = request.args.get('search', '').lower()
page = int(request.args.get('page', 1))
per_page = 10 # 每页显示10条数据
apps = load_apps()
categories = load_categories()
settings = load_settings()
if category_filter:
# 应用筛选
filtered_apps = []
for app in apps:
# 检查是否匹配主分类或子分类
if (app['category']['main'] == category_filter or
# 分类筛选
if category_filter and not (app['category']['main'] == category_filter or
app['category']['sub'] == category_filter or
(category_filter in categories and app['category']['main'] == category_filter)):
filtered_apps.append(app)
apps = filtered_apps
continue
return render_template('manage.html', apps=apps, settings=settings, categories=categories)
# 搜索筛选
if search_query and not (search_query in app['title'].lower() or
search_query in app['url'].lower() or
search_query in app.get('description', '').lower() or
search_query in app['category']['main'].lower() or
search_query in app['category']['sub'].lower()):
continue
filtered_apps.append(app)
# 分页处理
total_pages = ceil(len(filtered_apps) / per_page)
paginated_apps = filtered_apps[(page - 1) * per_page: page * per_page]
return render_template('manage.html',
apps=paginated_apps,
settings=settings,
categories=categories,
current_page=page,
total_pages=total_pages,
search_query=search_query,
category_filter=category_filter)
@app.route('/')
def navigation():

View File

@ -8,7 +8,8 @@
"show_logo": true,
"logo_type": "image",
"logo_icon": "fa-solid fa-th-list",
"logo_image": "/upload/logo/5378dda810964da9a7515ec844628738.png",
"logo_image": "/upload/logo/b2c128cf2d4e47daa349c5e7f38c932c.png",
"dark_bg_rotate": false,
"admin_password_hash": "scrypt:32768:8:1$mPFCfRRzOrcjE6z3$e72ef50a2d3f7292f64bcfc5e21f32c95ea8665414ea8d5f6b216735d68f151166c99fae21132c7949bd92ea32041f969cd4a471adb110a99328089541f7dccb"
"admin_password_hash": "scrypt:32768:8:1$mPFCfRRzOrcjE6z3$e72ef50a2d3f7292f64bcfc5e21f32c95ea8665414ea8d5f6b216735d68f151166c99fae21132c7949bd92ea32041f969cd4a471adb110a99328089541f7dccb",
"footer_html": "<div class=\"flex flex-col items-center text-slate-400 text-sm py-4\" style=\"margin-top:100px\">\r\n <div class=\"flex items-center\">\r\n <span>© 2023 AIDaohang. All rights reserved.</span>\r\n <span class=\"mx-2\">|</span>\r\n <span>Powered by <a href=\"https://github.com\" target=\"_blank\" class=\"hover:text-slate-200 ml-1\">AIDaohang</a></span>\r\n </div>\r\n</div>"
}

View File

@ -32,7 +32,25 @@
</div>
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">附件列表</h5>
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="border-bottom pb-2 mb-0">附件列表</h5>
<form class="d-flex" method="GET">
<select name="type" class="form-select me-2" style="width: 120px;">
<option value="all" {% if type_filter == 'all' %}selected{% endif %}>所有类型</option>
<option value="logo" {% if type_filter == 'logo' %}selected{% endif %}>Logo</option>
<option value="background" {% if type_filter == 'background' %}selected{% endif %}>背景图片</option>
<option value="video" {% if type_filter == 'video' %}selected{% endif %}>背景视频</option>
<option value="icon" {% if type_filter == 'icon' %}selected{% endif %}>图标图片</option>
</select>
<input type="text" name="search" class="form-control me-2" placeholder="搜索文件名..." value="{{ search_query }}">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
</button>
{% if type_filter != 'all' or search_query %}
<a href="{{ url_for('manage_attachments') }}" class="btn btn-secondary ms-2">重置</a>
{% endif %}
</form>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
@ -83,12 +101,37 @@
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">暂无附件</td>
<td colspan="5" class="text-center">没有找到匹配的附件</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页导航 -->
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item {% if current_page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('manage_attachments', page=current_page-1, type=type_filter, search=search_query) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for page_num in range(1, total_pages + 1) %}
<li class="page-item {% if page_num == current_page %}active{% endif %}">
<a class="page-link" href="{{ url_for('manage_attachments', page=page_num, type=type_filter, search=search_query) }}">{{ page_num }}</a>
</li>
{% endfor %}
<li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('manage_attachments', page=current_page+1, type=type_filter, search=search_query) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}
</div>
<div class="mt-4">
<a href="{{ url_for('index') }}" class="btn btn-secondary">

View File

@ -13,7 +13,7 @@
<link rel="stylesheet" href="/static/css/all.min.css">
<style>
body {
padding-top: 20px; /* 修改了这里 */
padding-top: 0px;
padding-bottom: 40px;
}
.navbar {
@ -120,9 +120,9 @@
{% block content %}{% endblock %}
<footer class="footer">
<p>© 2025 导航管理系统</p>
</footer>
<footer class="footer">
{% include 'footer.html' %}
</footer>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>

View File

@ -17,14 +17,19 @@
<label class="form-label">URL</label>
<input type="url" name="url" class="form-control" value="{{ app.url }}" required>
</div>
<!-- 在图标选择部分修改 -->
<div class="mb-3">
<!-- 修改后的图标选择部分 -->
<div class="mb-3">
<label class="form-label">图标</label>
<input type="hidden" name="icon" id="selectedIcon" value="" required>
<input type="hidden" name="icon" id="selectedIcon" value="{{ app.icon }}" required>
<div class="d-flex align-items-center mb-3">
<div class="icon-preview me-3">
<i id="iconPreview" class="fas fa-question-circle fa-2x"></i>
{% if app.icon.startswith('/upload/icon/') %}
<img id="iconImagePreview" src="{{ app.icon }}" style="max-width: 32px; max-height: 32px;">
<i id="iconPreview" class="fas fa-question-circle fa-2x" style="display: none;"></i>
{% else %}
<i id="iconPreview" class="{{ app.icon }} fa-2x"></i>
<img id="iconImagePreview" src="" style="display: none; max-width: 32px; max-height: 32px;">
{% endif %}
</div>
<button type="button" class="btn btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#iconModal">
<i class="fas fa-icons me-2"></i>选择图标
@ -33,10 +38,10 @@
<i class="fas fa-image me-2"></i>选择图片
</button>
</div>
</div>
</div>
<!-- 添加图标图片选择模态框 -->
<div class="modal fade" id="iconImageModal" tabindex="-1" aria-labelledby="iconImageModalLabel" aria-hidden="true">
<!-- 图标图片选择模态框 -->
<div class="modal fade" id="iconImageModal" tabindex="-1" aria-labelledby="iconImageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
@ -74,7 +79,7 @@
</div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">主分类</label>
<select name="main_category" id="main_category" class="form-select" required>
@ -205,7 +210,10 @@
document.getElementById('confirmIcon').addEventListener('click', function() {
if (selectedIcon) {
document.getElementById('selectedIcon').value = selectedIcon;
document.getElementById('iconPreview').className = selectedIcon + ' fa-2x';
const iconPreview = document.getElementById('iconPreview');
iconPreview.className = selectedIcon + ' fa-2x';
iconPreview.style.display = 'inline';
document.getElementById('iconImagePreview').style.display = 'none';
bootstrap.Modal.getInstance(document.getElementById('iconModal')).hide();
} else {
alert('请先选择一个图标');
@ -238,19 +246,15 @@
});
// 初始化时高亮已选图标
if (selectedIcon) {
if (selectedIcon && !selectedIcon.startsWith('/upload/icon/')) {
const selectedItem = document.querySelector(`.icon-item[data-icon="${selectedIcon}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
}
}
});
// 图标图片选择功能
let selectedIconImage = '';
let selectedIconImageName = '';
// 图标图片搜索功能
document.getElementById('iconImageSearch').addEventListener('input', function() {
// 图标图片搜索功能
document.getElementById('iconImageSearch').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const iconImageItems = document.querySelectorAll('.icon-image-item');
@ -262,10 +266,10 @@ document.getElementById('iconImageSearch').addEventListener('input', function()
item.closest('.col').style.display = 'none';
}
});
});
});
// 图标图片点击选择
document.querySelectorAll('.icon-image-item').forEach(item => {
// 图标图片点击选择
document.querySelectorAll('.icon-image-item').forEach(item => {
item.addEventListener('click', function() {
// 移除所有选中状态
document.querySelectorAll('.icon-image-item').forEach(i => {
@ -280,21 +284,23 @@ document.querySelectorAll('.icon-image-item').forEach(item => {
// 更新底部显示
document.getElementById('selectedIconImageName').textContent = selectedIconImageName;
});
});
});
// 确认选择图标图片
document.getElementById('confirmIconImage').addEventListener('click', function() {
// 确认选择图标图片
document.getElementById('confirmIconImage').addEventListener('click', function() {
if (selectedIconImage) {
document.getElementById('selectedIcon').value = '/upload/icon/' + selectedIconImage;
document.getElementById('iconPreview').style.display = 'none';
const iconPath = '/upload/icon/' + selectedIconImage;
document.getElementById('selectedIcon').value = iconPath;
const iconImagePreview = document.getElementById('iconImagePreview');
iconImagePreview.src = '/upload/icon/' + selectedIconImage;
iconImagePreview.src = iconPath;
iconImagePreview.style.display = 'inline';
document.getElementById('iconPreview').style.display = 'none';
bootstrap.Modal.getInstance(document.getElementById('iconImageModal')).hide();
} else {
alert('请先选择一张图片');
}
});
});
});
</script>
<style>
@ -325,12 +331,29 @@ document.getElementById('confirmIconImage').addEventListener('click', function()
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
#iconGrid {
.icon-image-item {
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.icon-image-item:hover {
background-color: #f8f9fa;
border-color: #dee2e6;
}
.icon-image-item.selected {
background-color: #e7f1ff;
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
#iconGrid, #iconImageGrid {
max-height: 60vh;
overflow-y: auto;
}
#selectedIconName {
#selectedIconName, #selectedIconImageName {
font-weight: bold;
color: #0d6efd;
}

6
templates/footer.html Normal file
View File

@ -0,0 +1,6 @@
<!-- templates/footer.html -->
{% if settings and settings.footer_html %}
{{ settings.footer_html|safe }}
{% else %}
<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>
{% endif %}

View File

@ -22,8 +22,8 @@
--ai-color: #f8961e;
--tooltip-bg: rgb(149 236 105);
--tooltip-text: black;
--bg-image: none; /* 修改为none */
--dark-bg-image: none; /* 修改为none */
--bg-image: none;
--dark-bg-image: none;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
@ -162,7 +162,7 @@
}
/* 右下角按钮组 */
.floating-buttons {
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
@ -170,8 +170,8 @@
display: flex;
flex-direction: column;
gap: 10px;
}
.floating-btn {
}
.floating-btn {
width: 40px;
height: 40px;
border-radius: 50%;
@ -186,14 +186,14 @@
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
transition: all 0.3s;
text-decoration: none;
}
}
.floating-btn:hover {
.floating-btn:hover {
transform: scale(1.1);
}
}
/* 按钮提示文字 */
.floating-btn::after {
/* 按钮提示文字 */
.floating-btn::after {
content: attr(title);
position: absolute;
right: 50px;
@ -206,109 +206,109 @@
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.floating-btn:hover::after {
}
.floating-btn:hover::after {
opacity: 1;
}
}
/* 暗黑模式下的按钮样式 */
body.dark-theme .floating-btn {
/* 暗黑模式下的按钮样式 */
body.dark-theme .floating-btn {
background: #444;
color: #eee;
}
}
/* 简洁模式卡片样式 */
.app-item.compact {
/* 简洁模式卡片样式 */
.app-item.compact {
padding: 10px 15px;
height: 50px;
flex-direction: row;
align-items: center;
}
}
.app-item.compact .app-icon {
.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 {
.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 {
/* 暗黑主题 */
body.dark-theme {
background-color: #121212;
color: #e0e0e0;
--tooltip-bg: rgba(30, 30, 30, 0.95);
--tooltip-text: #e0e0e0;
}
}
/* 新增暗黑模式下标题颜色设置 */
body.dark-theme h1 {
/* 新增暗黑模式下标题颜色设置 */
body.dark-theme h1 {
color: white !important;
}
body.dark-theme .app-item {
}
body.dark-theme .app-item {
background-color: #1e1e1edb;
border-color: #333333a1;
color: #e0e0e0;
}
body.dark-theme .category-title {
}
body.dark-theme .category-title {
border-bottom-color: #333;
color: #e0e0e0;
}
body.dark-theme .filter-container {
}
body.dark-theme .filter-container {
background-color: #121212d4 !important;
color: #e0e0e0;
}
body.dark-theme .secondary-filters-container {
}
body.dark-theme .secondary-filters-container {
background-color: #12121200 !important;
color: #e0e0e0;
}
body.dark-theme .filter-btn {
}
body.dark-theme .filter-btn {
background-color: #2d2d2d !important;
color: #e0e0e0 !important;
}
body.dark-theme .search-input {
}
body.dark-theme .search-input {
background-color: #1e1e1e;
color: #e0e0e0;
border-color: #333;
}
body.dark-theme .search-results {
}
body.dark-theme .search-results {
background-color: #1e1e1e;
color: #e0e0e0;
}
body.dark-theme .search-result-item {
}
body.dark-theme .search-result-item {
border-bottom-color: #333;
color: #e0e0e0;
}
body.dark-theme .search-result-item:hover {
}
body.dark-theme .search-result-item:hover {
background-color: #2a2a2a;
}
body.dark-theme .floating-btn {
}
body.dark-theme .floating-btn {
background-color: #444;
color: #e0e0e0;
}
body.dark-theme .floating-btn:hover {
}
body.dark-theme .floating-btn:hover {
background-color: #555;
}
body.dark-theme .app-url {
}
body.dark-theme .app-url {
color: #aaa !important;
}
}
.filter-container {
/* 修改后的筛选容器样式 - 支持横向滚动 */
.filter-container {
display: flex;
flex-direction: column;
gap: 10px;
@ -323,13 +323,37 @@ body.dark-theme .app-url {
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: wrap;
gap: 10px;
justify-content: center;
}
/* 修改后的筛选行样式 - 支持横向滚动 */
.filter-row {
display: flex;
flex-wrap: nowrap;
gap: 10px;
overflow-x: auto;
padding-bottom: 10px;
scrollbar-width: thin;
scrollbar-color: var(--primary-color) transparent;
justify-content: center; /* 默认居中 */
padding: 0 20px; /* 添加内边距 */
}
/* 当内容超出时靠左 */
.filter-row.scrollable {
justify-content: flex-start;
}
/* 自定义滚动条样式 */
.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;
@ -340,6 +364,8 @@ body.dark-theme .app-url {
font-size: 14px;
display: flex;
align-items: center;
white-space: nowrap; /* 禁止按钮内文字换行 */
flex-shrink: 0; /* 禁止按钮缩小 */
}
.filter-btn:hover {
transform: translateY(-2px);
@ -348,17 +374,17 @@ body.dark-theme .app-url {
.filter-btn.active {
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
/* 一级标签样式 */
.filter-btn[data-level="1"] {
}
/* 一级标签样式 */
.filter-btn[data-level="1"] {
font-size: 15px;
padding: 4px 18px;
}
/* 二级标签样式 */
.filter-btn[data-level="2"] {
}
/* 二级标签样式 */
.filter-btn[data-level="2"] {
font-size: 13px;
padding: 6px 12px;
}
}
/* 分类选择器颜色 */
.filter-btn.dev {
background-color: var(--dev-color, #4cc9f0) !important;
@ -376,35 +402,58 @@ body.dark-theme .app-url {
background-color: var(--ai-color, #f8961e) !important;
}
.caret {
.caret {
margin-right: 8px;
transition: transform 0.2s;
}
.caret.down {
}
.caret.down {
transform: rotate(90deg);
}
}
/* 修改后的二级筛选容器样式 - 支持横向滚动 */
.secondary-filters-container {
width: 100%;
display: none;
flex-wrap: wrap;
flex-wrap: nowrap;
gap: 10px;
justify-content: center;
padding: 10px 0;
transition: background-color 0.3s ease;
overflow-x: auto;
padding-bottom: 10px;
scrollbar-width: thin;
scrollbar-color: var(--primary-color) transparent;
justify-content: center; /* 默认居中 */
padding: 0 20px; /* 添加内边距 */
}
.secondary-filters-container.show {
/* 当内容超出时靠左 */
.secondary-filters-container.scrollable {
justify-content: flex-start;
}
/* 自定义二级筛选容器的滚动条样式 */
.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 {
}
.secondary-filters-wrapper {
width: 100%;
position: relative;
min-height: 50px;
}
.app-list-container {
min-height: 30px;
}
.app-list-container {
margin-top: 20px;
position: relative;
}
.category-title {
}
.category-title {
font-size: 20px;
font-weight: bold;
margin: 30px 0 15px 0;
@ -413,39 +462,39 @@ body.dark-theme .app-url {
display: flex;
align-items: center;
transition: color 0.3s ease, border-color 0.3s ease;
}
.category-title i {
}
.category-title i {
margin-right: 10px;
}
.app-group {
}
.app-group {
margin-bottom: 30px;
}
.app-list {
}
.app-list {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
@media (max-width: 1200px) {
}
@media (max-width: 1200px) {
.app-list {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 992px) {
}
@media (max-width: 992px) {
.app-list {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
}
@media (max-width: 768px) {
.app-list {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
}
@media (max-width: 576px) {
.app-list {
grid-template-columns: 1fr;
}
}
.app-item {
}
.app-item {
background-color: #ffffff69;
border-radius: 12px;
padding: 20px;
@ -458,13 +507,13 @@ body.dark-theme .app-url {
text-decoration: none;
color: inherit;
position: relative;
}
.app-item:hover {
}
.app-item:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
border-color: var(--primary-color);
}
.app-icon {
}
.app-icon {
width: 40px;
height: 40px;
margin-right: 15px;
@ -477,46 +526,46 @@ body.dark-theme .app-url {
font-size: 18px;
color: var(--primary-color);
transition: background-color 0.3s ease;
overflow: hidden; /* 添加这行确保内容不会溢出 */
}
overflow: hidden;
}
.app-icon img {
.app-icon img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
margin: 0 !important; /* 修改margin为0 */
}
.app-info {
margin: 0 !important;
}
.app-info {
flex: 1;
}
.app-title {
}
.app-title {
font-weight: 600;
margin-bottom: 4px;
transition: color 0.3s ease;
}
.app-url {
}
.app-url {
font-size: 13px;
color: #6c757d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.3s ease;
}
.app-tags {
}
.app-tags {
display: flex;
gap: 5px;
margin-top: 5px;
flex-wrap: wrap;
}
.app-tag {
}
.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;
@ -534,16 +583,16 @@ body.dark-theme .app-url {
background-color: var(--ai-color, #f8961e) !important;
}
.no-results {
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: 6c757d;
}
.app-group.hidden {
}
.app-group.hidden {
display: none;
}
.loading-spinner {
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
@ -552,17 +601,17 @@ body.dark-theme .app-url {
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
}
@keyframes spin {
to { transform: rotate(360deg); }
}
}
/* 修改后的聊天气泡提示框样式 */
.app-description {
/* 修改后的聊天气泡提示框样式 */
.app-description {
position: absolute;
bottom: calc(100% + 10px);
left: 20px; /* 调整左侧位置 */
transform: none; /* 移除水平居中转换 */
left: 20px;
transform: none;
background-color: var(--tooltip-bg);
color: var(--tooltip-text);
padding: 10px 15px;
@ -576,22 +625,29 @@ body.dark-theme .app-url {
z-index: 100;
text-align: left;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
}
/* 修改后的聊天气泡小三角 - 移到左侧 */
.app-description::after {
/* 修改后的聊天气泡小三角 - 移到左侧 */
.app-description::after {
content: '';
position: absolute;
top: 100%;
left: 20px; /* 调整与气泡对齐 */
transform: none; /* 移除水平居中转换 */
left: 20px;
transform: none;
border-width: 8px;
border-style: solid;
border-color: var(--tooltip-bg) transparent transparent transparent;
}
.app-item:hover .app-description {
}
.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>
@ -612,12 +668,12 @@ body.dark-theme .app-url {
</h1>
</div>
<div class="search-container">
<div class="search-container">
<input type="text" class="search-input" placeholder="搜索应用、网址或分类..." id="searchInput">
<div class="search-results" id="searchResults"></div>
</div>
</div>
<div class="filter-container">
<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>
@ -627,14 +683,14 @@ body.dark-theme .app-url {
<div class="secondary-filters-wrapper" id="secondaryFiltersWrapper">
<!-- 二级标签容器将通过JS动态生成 -->
</div>
</div>
</div>
<div class="app-list-container" id="appListContainer">
<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>
<div class="floating-buttons">
<a href="/login?next=/" class="floating-btn" id="loginBtn" title="登录/退出"><i class="fas fa-sign-in-alt"></i></a>
@ -643,7 +699,10 @@ body.dark-theme .app-url {
<button class="floating-btn" id="compactToggle" title="简洁模式">📱</button>
</div>
<script>
<footer class="footer">
{% include 'footer.html' %}
</footer>
<script>
// 全局变量存储应用和分类数据
let apps = [];
let categories = {};
@ -761,10 +820,10 @@ body.dark-theme .app-url {
const weightB = b[1].weight || 0;
return weightB - weightA; // 降序排列
})
);
);
// 对每个主分类下的子分类也按权重排序
Object.values(categories).forEach(cat => {
// 对每个主分类下的子分类也按权重排序
Object.values(categories).forEach(cat => {
if (cat.sub) {
cat.sub = Object.fromEntries(
Object.entries(cat.sub).sort((a, b) => {
@ -774,26 +833,25 @@ Object.values(categories).forEach(cat => {
})
);
}
});
});
// 渲染界面
renderFilters();
renderApps();
loadingSpinner.style.display = 'none';
primaryFilters.querySelector('[data-filter="all"]').textContent = '全部';
} catch (error) {
// 渲染界面
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() {
// 更新登录按钮状态
function updateLoginButton() {
if (isLoggedIn) {
loginBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
loginBtn.href = '/logout';
@ -805,15 +863,15 @@ function updateLoginButton() {
loginBtn.title = '登录';
adminBtn.href = '/login?next=/manage'; // 未登录时跳转到登录页面
}
}
}
// 根据分类ID获取对应的颜色类
function getColorForCategory(categoryId) {
// 根据分类ID获取对应的颜色类
function getColorForCategory(categoryId) {
return categoryId;
}
}
// 生成筛选按钮
function renderFilters() {
// 生成筛选按钮
function renderFilters() {
// 生成一级标签
primaryFilters.innerHTML = '';
const allPrimaryBtn = document.createElement('button');
@ -859,10 +917,10 @@ function renderFilters() {
// 初始加载时显示所有二级标签
showAllSecondaryFilters();
}
}
// 设置所有一级标签箭头的展开/收起状态
function setAllPrimaryCaretDown(shouldDown) {
// 设置所有一级标签箭头的展开/收起状态
function setAllPrimaryCaretDown(shouldDown) {
const primaryBtns = primaryFilters.querySelectorAll('[data-level="1"]');
primaryBtns.forEach(btn => {
if (btn.dataset.filter !== 'all') {
@ -876,10 +934,10 @@ function setAllPrimaryCaretDown(shouldDown) {
}
}
});
}
}
// 设置特定一级标签箭头的展开/收起状态
function setPrimaryCaretDown(primaryId, shouldDown) {
// 设置特定一级标签箭头的展开/收起状态
function setPrimaryCaretDown(primaryId, shouldDown) {
const btn = primaryFilters.querySelector(`[data-filter="${primaryId}"]`);
if (btn) {
const caret = btn.querySelector('.caret');
@ -891,10 +949,10 @@ function setPrimaryCaretDown(primaryId, shouldDown) {
}
}
}
}
}
// 显示所有二级标签
function showAllSecondaryFilters() {
// 显示所有二级标签
function showAllSecondaryFilters() {
collapseAllSecondary();
const container = document.createElement('div');
@ -931,10 +989,10 @@ function showAllSecondaryFilters() {
secondaryFiltersWrapper.innerHTML = '';
secondaryFiltersWrapper.appendChild(container);
currentExpandedPrimary = null;
}
}
// 展开二级标签
function expandSecondary(primaryFilterId) {
// 展开二级标签
function expandSecondary(primaryFilterId) {
collapseAllSecondary();
const primaryBtn = primaryFilters.querySelector(`[data-filter="${primaryFilterId}"]`);
@ -971,16 +1029,16 @@ function expandSecondary(primaryFilterId) {
secondaryFiltersWrapper.appendChild(container);
currentExpandedPrimary = primaryFilterId;
}
}
}
// 收起所有二级标签
function collapseAllSecondary() {
// 收起所有二级标签
function collapseAllSecondary() {
secondaryFiltersWrapper.innerHTML = '';
currentExpandedPrimary = null;
}
}
// 设置当前筛选条件
function setFilter(primaryFilter, secondaryFilter) {
// 设置当前筛选条件
function setFilter(primaryFilter, secondaryFilter) {
currentPrimaryFilter = primaryFilter;
currentSecondaryFilter = secondaryFilter;
@ -1011,10 +1069,10 @@ function setFilter(primaryFilter, secondaryFilter) {
// 重新渲染应用列表
renderApps();
}
}
// 渲染应用列表
function renderApps() {
// 渲染应用列表
function renderApps() {
appListContainer.innerHTML = '';
// 按一级标签分组
@ -1067,7 +1125,7 @@ function renderApps() {
appList.className = 'app-list';
// 添加应用卡片
filteredGroupApps.forEach(app => {
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;
@ -1101,7 +1159,7 @@ filteredGroupApps.forEach(app => {
appItem.prepend(descriptionDiv);
appList.appendChild(appItem);
});
});
groupDiv.appendChild(appList);
appListContainer.appendChild(groupDiv);
@ -1114,10 +1172,10 @@ filteredGroupApps.forEach(app => {
noResults.textContent = '没有找到匹配的应用';
appListContainer.appendChild(noResults);
}
}
}
// 辅助函数:计算对比色
function getContrastColor(hexColor) {
// 辅助函数:计算对比色
function getContrastColor(hexColor) {
if (!hexColor) return '#ffffff';
// 转换hex颜色为RGB
@ -1130,17 +1188,17 @@ function getContrastColor(hexColor) {
// 根据亮度返回黑色或白色
return brightness > 128 ? '#000000' : '#ffffff';
}
}
// 全局设置
let settings = {
// 全局设置
let settings = {
theme: 'auto',
card_style: 'normal',
search_history: []
};
};
// 加载设置
async function loadSettings() {
// 加载设置
async function loadSettings() {
try {
// 检查是否登录
isLoggedIn = await checkLoginStatus();
@ -1189,10 +1247,10 @@ async function loadSettings() {
} catch (error) {
console.error('加载设置失败:', error);
}
}
}
// 设置背景图片
function setBackgroundImages(lightImage, darkImage) {
// 设置背景图片
function setBackgroundImages(lightImage, darkImage) {
// 移除现有的视频背景
document.querySelectorAll('.video-bg-container').forEach(el => el.remove());
@ -1240,10 +1298,10 @@ function setBackgroundImages(lightImage, darkImage) {
setTimeout(() => {
document.body.classList.add('bg-loaded');
}, 100);
}
}
// 检查登录状态
async function checkLoginStatus() {
// 检查登录状态
async function checkLoginStatus() {
try {
const response = await fetch('/api/check_login');
const data = await response.json();
@ -1252,14 +1310,14 @@ async function checkLoginStatus() {
console.error('检查登录状态失败:', error);
return false;
}
}
}
// 应用设置
function applySettings() {
// 应用设置
function applySettings() {
// 应用主题
document.body.classList.remove('dark-theme', 'light-theme');
if (settings.theme === 'dark' ||
(settings.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
(settings.theme === 'auto' && window.matchMedia('(prefers-color-sscheme: dark)').matches)) {
document.body.classList.add('dark-theme');
// 显示/隐藏视频背景
@ -1280,10 +1338,10 @@ function applySettings() {
// 更新主题按钮状态
updateThemeButtonIcon();
}
}
// 更新主题按钮图标
function updateThemeButtonIcon() {
// 更新主题按钮图标
function updateThemeButtonIcon() {
const themeToggle = document.getElementById('themeToggle');
if (document.body.classList.contains('dark-theme')) {
themeToggle.textContent = '☀️';
@ -1292,10 +1350,10 @@ function updateThemeButtonIcon() {
themeToggle.textContent = '🌙';
themeToggle.title = '切换至暗黑模式';
}
}
}
// 保存设置
function saveSettings() {
// 保存设置
function saveSettings() {
if (isLoggedIn) {
fetch('/api/settings', {
method: 'POST',
@ -1313,10 +1371,10 @@ function saveSettings() {
};
localStorage.setItem('navSettings', JSON.stringify(settingsToSave));
}
}
}
// 搜索功能
function setupSearch() {
// 搜索功能
function setupSearch() {
const searchInput = document.getElementById('searchInput');
const searchResults = document.getElementById('searchResults');
let searchTimeout;
@ -1354,7 +1412,7 @@ function setupSearch() {
searchResults.innerHTML = '<div class="search-result-item">无搜索结果</div>';
searchResults.style.display = 'block';
}
});
});
} else {
searchResults.style.display = 'none';
}
@ -1366,10 +1424,10 @@ function setupSearch() {
searchResults.style.display = 'none';
}
});
}
}
// 主题切换功能
function setupThemeSwitcher() {
// 主题切换功能
function setupThemeSwitcher() {
const themeToggle = document.getElementById('themeToggle');
themeToggle.addEventListener('click', () => {
@ -1393,20 +1451,20 @@ function setupThemeSwitcher() {
updateThemeButtonIcon();
saveSettings();
});
}
}
// 简洁模式切换
function setupCompactToggle() {
// 简洁模式切换
function setupCompactToggle() {
const compactToggle = document.getElementById('compactToggle');
compactToggle.addEventListener('click', () => {
settings.card_style = settings.card_style === 'compact' ? 'normal' : 'compact';
applySettings();
saveSettings();
});
}
}
// 后台管理按钮点击事件
function setupAdminButton() {
// 后台管理按钮点击事件
function setupAdminButton() {
adminBtn.addEventListener('click', async (e) => {
if (!isLoggedIn) {
e.preventDefault();
@ -1420,16 +1478,38 @@ function setupAdminButton() {
}
}
});
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
loadSettings();
setupSearch();
setupThemeSwitcher();
setupCompactToggle();
setupAdminButton();
});
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>

View File

@ -10,6 +10,15 @@
<i class="fas fa-plus"></i> 添加应用
</a>
</div>
<div>
<form class="d-flex" method="GET">
<input type="hidden" name="category" value="{{ category_filter }}">
<input type="text" name="search" class="form-control form-control-sm me-2" placeholder="搜索应用..." value="{{ search_query }}">
<button type="submit" class="btn btn-sm btn-light">
<i class="fas fa-search"></i>
</button>
</form>
</div>
</div>
<div class="card-body">
<div class="mb-3">
@ -31,6 +40,9 @@
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">筛选</button>
{% if category_filter or search_query %}
<a href="{{ url_for('index') }}" class="btn btn-secondary ms-2">重置</a>
{% endif %}
</div>
</form>
</div>
@ -86,10 +98,39 @@
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">没有找到匹配的应用</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页导航 -->
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item {% if current_page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('index', page=current_page-1, category=category_filter, search=search_query) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for page_num in range(1, total_pages + 1) %}
<li class="page-item {% if page_num == current_page %}active{% endif %}">
<a class="page-link" href="{{ url_for('index', page=page_num, category=category_filter, search=search_query) }}">{{ page_num }}</a>
</li>
{% endfor %}
<li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('index', page=current_page+1, category=category_filter, search=search_query) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -280,6 +280,27 @@
</div>
</div>
<!-- 页脚设置 -->
<div class="card mb-4 border-light shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-code me-2"></i>页脚设置</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="footer_html" class="form-label">自定义页脚HTML代码</label>
<textarea class="form-control" id="footer_html" name="footer_html" rows="4">{{ settings.footer_html if settings.footer_html else '' }}</textarea>
<div class="form-text">支持HTML代码可用于添加版权信息、统计代码等</div>
</div>
<div class="preview-area mt-3 p-3 bg-light rounded">
<h6>预览效果:</h6>
<div id="footer_preview">
{{ settings.footer_html|safe if settings.footer_html else '' }}
</div>
</div>
</div>
</div>
<!-- 修改密码 -->
<div class="card mb-4 border-light shadow-sm">
<div class="card-header bg-light">
@ -464,6 +485,10 @@
</div>
<script>
// 页脚预览
document.getElementById('footer_html').addEventListener('input', function() {
document.getElementById('footer_preview').innerHTML = this.value;
});
// 全局选择函数
function selectLogo(filename) {
document.getElementById('selected_logo').value = filename;