优化移动端样式,优化后台逻辑

This commit is contained in:
wzj 2025-07-09 00:35:26 +08:00
parent ef39536ed9
commit 75c691c183
9 changed files with 1395 additions and 878 deletions

View File

@ -25,7 +25,7 @@
## 待开发功能 ⏳
### 界面优化
1. logo图标设置区分明亮和暗黑模式
2. 私有应用在首页添加标识
2. 私有应用在首页添加标识 - 已完成
3. 首页卡片右键菜单
4. 应用支持配置多个URL左键打开默认URL右键可选择URL进行复制地址或者打开或者编辑应用
5. 新增应用界面便捷增加分类
@ -37,10 +37,10 @@
2. 书签收藏工具
### BUG修复
1. 游客通过接口能查看私有应用
1. 游客通过接口能查看私有应用 - 已修复
### 移动端适配
1. 后台页面适配
1. 后台页面适配 - 已完成
### 批量操作
1. 应用批量选择功能:

42
app.py
View File

@ -1033,6 +1033,48 @@ def change_password(username, new_password):
else:
print("错误:只能修改管理员 (admin) 的密码")
@app.route('/app/add_from_category', methods=['POST'])
@login_required
def add_app_from_category():
categories = load_categories()
icons = load_icons()
settings = load_settings()
attachments = load_attachments()
if request.method == 'POST':
title = request.form['title']
url = request.form['url']
icon = request.form['icon']
description = request.form.get('description', '')
private = 'private' in request.form
main_category = request.form['main_category']
sub_category = request.form['sub_category']
new_app = {
"title": title,
"url": url,
"icon": icon,
"description": description,
"private": private,
"category": {
"main": main_category,
"sub": sub_category
}
}
apps = load_apps()
apps.append(new_app)
save_apps(apps)
flash('应用添加成功', 'success')
return redirect(url_for('manage_categories'))
return render_template('add_app.html',
categories=categories,
settings=settings,
icons=load_icons(),
attachments=attachments)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='导航系统管理工具')

View File

@ -63,5 +63,25 @@
"filename": "c77f6bbbb335.png",
"type": "icon",
"upload_time": "2025-07-06 20:22:28"
},
{
"filename": "hpCMDHrCJVq3.png",
"type": "logo",
"upload_time": "2025-07-08 21:54:48"
},
{
"filename": "ksDMQ8aEwEph.png",
"type": "logo",
"upload_time": "2025-07-08 21:55:09"
},
{
"filename": "gfpWMYOvTTuI.png",
"type": "logo",
"upload_time": "2025-07-08 21:55:20"
},
{
"filename": "G31hd5Kzu6t8.png",
"type": "logo",
"upload_time": "2025-07-08 21:55:29"
}
]

View File

@ -155,6 +155,11 @@
"name": "营养",
"color": "#4d908e",
"weight": 2
},
"swim": {
"name": "游泳",
"color": "#4895ef",
"weight": 4
}
},
"weight": 4,

View File

@ -1,15 +1,15 @@
{
"card_style": "compact",
"search_history": [],
"theme": "light",
"theme": "dark",
"bg_image": "/upload/background/5dd4f5d3cd7b.png",
"dark_bg_image": "/upload/background/d078c01de3be.png",
"site_title": "应用导航中心",
"show_logo": true,
"logo_type": "image",
"logo_icon": "fa-solid fa-th-list",
"logo_image": "/upload/logo/f40e2eb965b2.png",
"logo_image": "/upload/logo/hpCMDHrCJVq3.png",
"dark_bg_rotate": false,
"admin_password_hash": "pbkdf2:sha256:260000$ig0vNC2QdipLHgAy$6d734e5112bbdf94bc5cb49a38ca4764376ebb51c877a416d1a8f27d5fb5c415",
"footer_html": "<div class=\"flex flex-col items-center text-slate-400 text-sm py-4\" style=\"margin-top:10px\">\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>"
"footer_html": "<div class=\"flex flex-col items-center text-slate-400 text-sm py-4\" style=\"margin-top:40px\">\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

@ -34,6 +34,8 @@
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="border-bottom pb-2 mb-0">附件列表</h5>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<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>
@ -51,6 +53,9 @@
{% endif %}
</form>
</div>
<!-- 桌面端显示表格 -->
<div class="d-none d-md-block">
<div class="table-responsive">
<table class="table table-striped">
<thead>
@ -107,6 +112,60 @@
</tbody>
</table>
</div>
</div>
<!-- 移动端显示卡片 -->
<div class="d-md-none">
<div class="row">
{% for attachment in attachments %}
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-4">
{% if attachment.type == 'logo' %}
<img src="{{ url_for('uploaded_logo', filename=attachment.filename) }}" class="img-fluid rounded">
{% elif attachment.type == 'background' %}
<img src="{{ url_for('uploaded_background', filename=attachment.filename) }}" class="img-fluid rounded">
{% elif attachment.type == 'video' %}
<video width="100%" muted class="rounded">
<source src="{{ url_for('uploaded_video', filename=attachment.filename) }}" type="video/mp4">
</video>
{% else %}
<img src="{{ url_for('uploaded_icon', filename=attachment.filename) }}" class="img-fluid rounded">
{% endif %}
</div>
<div class="col-8">
<h6 class="mb-1">{{ attachment.filename }}</h6>
<p class="mb-1 text-muted small">
{% if attachment.type == 'logo' %}
Logo
{% elif attachment.type == 'background' %}
背景图片
{% elif attachment.type == 'video' %}
背景视频
{% else %}
图标图片
{% endif %}
</p>
<p class="mb-2 text-muted small">{{ attachment.upload_time }}</p>
<form method="POST" action="{{ url_for('delete_attachment_route', filename=attachment.filename) }}" class="mt-2">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除这个附件吗?')">
<i class="fas fa-trash"></i> 删除
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info text-center">没有找到匹配的附件</div>
</div>
{% endfor %}
</div>
</div>
<!-- 分页导航 -->
{% if total_pages > 1 %}

View File

@ -3,62 +3,187 @@
{% block title %}分类管理{% endblock %}
{% block content %}
<!-- 添加子分类模态框 -->
<div class="modal fade" id="addSubCategoryModal" tabindex="-1" aria-labelledby="addSubCategoryModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addSubCategoryModalLabel">添加子分类</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{{ url_for('add_sub_category') }}" id="subCategoryForm">
<div class="modal-body">
<input type="hidden" name="main_id" id="modalMainId">
<div class="mb-3">
<label for="sub_id" class="form-label">子分类 ID</label>
<input type="text" name="sub_id" class="form-control" required>
</div>
<div class="mb-3">
<label for="sub_name" class="form-label">子分类名称</label>
<input type="text" name="sub_name" class="form-control" required>
</div>
<div class="mb-3">
<label for="weight" class="form-label">权重</label>
<input type="number" name="weight" class="form-control" value="0">
</div>
<div class="mb-3">
<label for="color" class="form-label">颜色</label>
<input type="color" name="color" class="form-control form-control-color" value="#4895ef">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="sub_private_modal" name="private">
<label class="form-check-label" for="sub_private_modal">私有</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">添加</button>
</div>
</form>
</div>
</div>
</div>
<!-- 添加应用模态框 -->
<div class="modal fade" id="addAppModal" tabindex="-1" aria-labelledby="addAppModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addAppModalLabel">添加应用</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{{ url_for('add_app_from_category') }}">
<div class="modal-body">
<input type="hidden" name="main_category" id="modalAppMainCategory">
<input type="hidden" name="sub_category" id="modalAppSubCategory">
<div class="mb-3">
<label for="title" class="form-label">应用名称</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label for="url" class="form-label">URL</label>
<input type="text" name="url" class="form-control" required>
</div>
<div class="mb-3">
<label for="icon" class="form-label">图标</label>
<select name="icon" class="form-select" required>
{% for icon in icons %}
<option value="{{ icon }}">{{ icon }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea name="description" class="form-control"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="private" name="private">
<label class="form-check-label" for="private">私有</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">添加</button>
</div>
</form>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="mb-4">
<h5>添加主分类</h5>
<form method="post" action="{{ url_for('add_main_category') }}" class="d-flex align-items-center gap-2">
<input type="text" name="id" class="form-control" placeholder="主分类 ID" required style="width: 120px;">
<input type="text" name="name" class="form-control" placeholder="主分类名称" required style="width: 150px;">
<input type="number" name="weight" class="form-control" placeholder="权重" value="0" style="width: 80px;">
<input type="color" name="color" class="form-control form-control-color" value="#4361ee" title="选择颜色" style="width: 50px;">
<div class="form-check form-check-inline">
<form method="post" action="{{ url_for('add_main_category') }}">
<div class="row g-3">
<div class="col-12 col-md-2">
<input type="text" name="id" class="form-control" placeholder="主分类 ID" required>
</div>
<div class="col-12 col-md-3">
<input type="text" name="name" class="form-control" placeholder="主分类名称" required>
</div>
<div class="col-12 col-md-1">
<input type="number" name="weight" class="form-control" placeholder="权重" value="0">
</div>
<div class="col-12 col-md-1">
<input type="color" name="color" class="form-control form-control-color" value="#4361ee" title="选择颜色">
</div>
<div class="col-12 col-md-2 d-flex align-items-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="main_private" name="private">
<label class="form-check-label" for="main_private">私有</label>
</div>
<button type="submit" class="btn btn-primary">
</div>
<div class="col-12 col-md-1">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-plus"></i> 添加
</button>
</div>
</div>
</form>
</div>
<div class="mb-4">
<h5>添加子分类</h5>
<form method="post" action="{{ url_for('add_sub_category') }}" class="d-flex align-items-center gap-2">
<select name="main_id" class="form-select" required style="width: 150px;">
<form method="post" action="{{ url_for('add_sub_category') }}">
<div class="row g-3">
<div class="col-12 col-md-2">
<select name="main_id" class="form-select" required>
<option value="">选择主分类</option>
{% for main_id, cat in categories.items() %}
{% for main_id, cat in categories.items()|sort(attribute='1.weight', reverse=True) %}
<option value="{{ main_id }}">{{ cat.name }}</option>
{% endfor %}
</select>
<input type="text" name="sub_id" class="form-control" placeholder="子分类 ID" required style="width: 120px;">
<input type="text" name="sub_name" class="form-control" placeholder="子分类名称" required style="width: 150px;">
<input type="number" name="weight" class="form-control" placeholder="权重" value="0" style="width: 80px;">
<input type="color" name="color" class="form-control form-control-color" value="#4895ef" title="选择颜色" style="width: 50px;">
<div class="form-check form-check-inline">
</div>
<div class="col-12 col-md-2">
<input type="text" name="sub_id" class="form-control" placeholder="子分类 ID" required>
</div>
<div class="col-12 col-md-3">
<input type="text" name="sub_name" class="form-control" placeholder="子分类名称" required>
</div>
<div class="col-12 col-md-1">
<input type="number" name="weight" class="form-control" placeholder="权重" value="0">
</div>
<div class="col-12 col-md-1">
<input type="color" name="color" class="form-control form-control-color" value="#4895ef" title="选择颜色">
</div>
<div class="col-12 col-md-2 d-flex align-items-center">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="sub_private" name="private">
<label class="form-check-label" for="sub_private">私有</label>
</div>
<button type="submit" class="btn btn-success">
</div>
<div class="col-12 col-md-1">
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-plus"></i> 添加
</button>
</div>
</div>
</form>
</div>
<div>
<h5>已存在的分类</h5>
<!-- 桌面端显示 -->
<div class="d-none d-md-block">
<div class="list-group">
{% for main_id, cat in categories.items() %}
{% for main_id, cat in categories.items()|sort(attribute='1.weight', reverse=True) %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="d-flex justify-content-between align-items-center flex-wrap">
<div class="mb-2 mb-md-0">
<strong>{{ cat.name }}</strong>ID: {{ main_id }})
<span class="badge" style="background-color: {{ cat.color }}; color: white;">{{ cat.color }}</span>
<span class="badge" style="background-color: {{ cat.color }}; color: white;">{{ cat.get('weight', 0) }}</span>
{% if cat.get('private', False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</div>
<div>
<button class="btn btn-sm btn-success me-2"
data-bs-toggle="modal"
data-bs-target="#addSubCategoryModal"
onclick="setMainId('{{ main_id }}')">
<i class="fas fa-plus"></i> 添加子分类
</button>
<a href="{{ url_for('edit_main_category', main_id=main_id) }}"
class="btn btn-sm btn-primary me-2">
<i class="fas fa-edit"></i> 编辑
@ -71,16 +196,22 @@
</div>
</div>
<ul class="mt-2 list-group list-group-flush">
{% for sub_id, subData in cat.sub.items() %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
{% for sub_id, subData in cat.sub.items()|sort(attribute='1.weight', reverse=True) %}
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap">
<span class="mb-2 mb-md-0">
{{ subData.name }}ID: {{ sub_id }})
<span class="badge" style="background-color: {{ subData.color }}; color: white;">{{ subData.color }}</span>
<span class="badge" style="background-color: {{ subData.color }}; color: white;">{{ subData.get('weight', 0) }}</span>
{% if cat.get('sub_private', {}).get(sub_id, False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</span>
<div>
<button class="btn btn-sm btn-success me-2"
data-bs-toggle="modal"
data-bs-target="#addAppModal"
onclick="setCategory('{{ main_id }}', '{{ sub_id }}')">
<i class="fas fa-plus"></i> 添加应用
</button>
<a href="{{ url_for('edit_sub_category', main_id=main_id, sub_id=sub_id) }}"
class="btn btn-sm btn-primary me-2">
<i class="fas fa-edit"></i> 编辑
@ -99,6 +230,86 @@
</div>
</div>
<!-- 移动端显示 -->
<div class="d-md-none">
<div class="accordion" id="categoryAccordion">
{% for main_id, cat in categories.items()|sort(attribute='1.weight', reverse=True) %}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center" style="background-color: {{ cat.color }}; color: white;">
<div>
<strong>{{ cat.name }}</strong> (ID: {{ main_id }})
<span class="badge bg-light text-dark ms-2">{{ cat.get('weight', 0) }}</span>
{% if cat.get('private', False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</div>
<div>
<button class="btn btn-sm btn-light me-2" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ loop.index }}" aria-expanded="true" aria-controls="collapse{{ loop.index }}">
<i class="fas fa-chevron-down"></i>
</button>
</div>
</div>
<div id="collapse{{ loop.index }}" class="collapse show" data-bs-parent="#categoryAccordion">
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<button class="btn btn-sm btn-success flex-grow-1 me-2"
data-bs-toggle="modal"
data-bs-target="#addSubCategoryModal"
onclick="setMainId('{{ main_id }}')">
<i class="fas fa-plus"></i> 添加子分类
</button>
<a href="{{ url_for('edit_main_category', main_id=main_id) }}"
class="btn btn-sm btn-primary flex-grow-1 me-2">
<i class="fas fa-edit"></i> 编辑主分类
</a>
<a href="{{ url_for('delete_main_category', main_id=main_id) }}"
class="btn btn-sm btn-danger flex-grow-1"
onclick="return confirm('确认删除整个主分类?')">
<i class="fas fa-trash"></i> 删除
</a>
</div>
<h6 class="mb-3">子分类列表</h6>
{% for sub_id, subData in cat.sub.items()|sort(attribute='1.weight', reverse=True) %}
<div class="card mb-2">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<strong>{{ subData.name }}</strong> (ID: {{ sub_id }})
<span class="badge" style="background-color: {{ subData.color }}; color: white;">{{ subData.get('weight', 0) }}</span>
{% if cat.get('sub_private', {}).get(sub_id, False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</div>
</div>
<div class="d-flex justify-content-between">
<button class="btn btn-sm btn-success flex-grow-1 me-2"
data-bs-toggle="modal"
data-bs-target="#addAppModal"
onclick="setCategory('{{ main_id }}', '{{ sub_id }}')">
<i class="fas fa-plus"></i> 添加应用
</button>
<a href="{{ url_for('edit_sub_category', main_id=main_id, sub_id=sub_id) }}"
class="btn btn-sm btn-primary flex-grow-1 me-2">
<i class="fas fa-edit"></i> 编辑
</a>
<a href="{{ url_for('delete_sub_category', main_id=main_id, sub_id=sub_id) }}"
class="btn btn-sm btn-outline-danger flex-grow-1"
onclick="return confirm('确认删除该子分类?')">
<i class="fas fa-trash"></i> 删除
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="mt-4">
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回首页
@ -106,4 +317,15 @@
</div>
</div>
</div>
<script>
function setMainId(mainId) {
document.getElementById('modalMainId').value = mainId;
}
function setCategory(mainId, subId) {
document.getElementById('modalAppMainCategory').value = mainId;
document.getElementById('modalAppSubCategory').value = subId;
}
</script>
{% endblock %}

View File

@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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 }}">
@ -25,6 +25,9 @@
--bg-image: none;
--dark-bg-image: none;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
max-width: 1400px;
@ -33,6 +36,8 @@
background-color: #f8f9fa;
color: #333;
transition: background-color 0.3s ease, color 0.3s ease;
overflow-x: hidden;
width: 100%;
}
/* 新增私有卡片标记样式 */
@ -45,7 +50,7 @@
border-style: solid;
border-width: 0 30px 30px 0;
border-color: transparent #f8961e transparent transparent;
border-radius: 0 12px 0 0; /* 只圆角右上角 */
border-radius: 0 12px 0 0;
}
.private-badge::after {
@ -152,6 +157,8 @@
margin: 20px auto;
max-width: 600px;
position: relative;
width: 100%;
padding: 0 15px;
}
.search-input {
width: 100%;
@ -164,7 +171,7 @@
}
.search-results {
position: absolute;
width: 100%;
width: calc(100% - 30px);
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
@ -237,9 +244,24 @@
/* 暗黑模式下的按钮样式 */
body.dark-theme .floating-btn {
background: #444;
color: #eee;
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;
@ -330,7 +352,7 @@
color: #aaa !important;
}
/* 修改后的筛选容器样式 - 支持横向滚动 */
/* 修改后的筛选容器样式 */
.filter-container {
display: flex;
flex-direction: column;
@ -346,9 +368,14 @@
padding: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease, color 0.3s ease;
margin-left: -15px;
margin-right: -15px;
padding-left: 20px;
padding-right: 20px;
width: calc(100% + 30px);
}
/* 修改后的筛选行样式 - 支持横向滚动 */
/* 修改后的筛选行样式 */
.filter-row {
display: flex;
flex-wrap: nowrap;
@ -357,10 +384,13 @@
padding-bottom: 10px;
scrollbar-width: thin;
scrollbar-color: #c2c3c9d6;
justify-content: center; /* 默认居中 */
padding: 0 20px; /* 添加内边距 */
justify-content: center;
padding: 0 15px;
scroll-behavior: smooth;
position: relative;
margin-left: -10px;
margin-right: -10px;
width: calc(100% + 20px);
}
/* 当内容超出时靠左 */
@ -417,8 +447,8 @@ body.dark-theme .filter-row::after {
font-size: 14px;
display: flex;
align-items: center;
white-space: nowrap; /* 禁止按钮内文字换行 */
flex-shrink: 0; /* 禁止按钮缩小 */
white-space: nowrap;
flex-shrink: 0;
}
.filter-btn:hover {
transform: translateY(-2px);
@ -463,7 +493,7 @@ body.dark-theme .filter-row::after {
transform: rotate(90deg);
}
/* 修改后的二级筛选容器样式 - 支持横向滚动 */
/* 修改后的二级筛选容器样式 */
.secondary-filters-container {
width: 100%;
display: none;
@ -473,10 +503,13 @@ body.dark-theme .filter-row::after {
padding-bottom: 10px;
scrollbar-width: thin;
scrollbar-color: #c2c3c9d6;
justify-content: center; /* 默认居中 */
padding: 0 20px; /* 添加内边距 */
justify-content: center;
padding: 0 15px;
scroll-behavior: smooth;
position: relative;
margin-left: -10px;
margin-right: -10px;
width: calc(100% + 20px);
}
/* 私有主分类 */
@ -542,6 +575,7 @@ body.dark-theme .secondary-filters-container::after {
.app-list-container {
margin-top: 20px;
position: relative;
width: 100%;
}
.category-title {
font-size: 20px;
@ -558,11 +592,13 @@ body.dark-theme .secondary-filters-container::after {
}
.app-group {
margin-bottom: 30px;
width: 100%;
}
.app-list {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
width: 100%;
}
@media (max-width: 1200px) {
.app-list {
@ -578,11 +614,34 @@ body.dark-theme .secondary-filters-container::after {
.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;
@ -597,6 +656,7 @@ body.dark-theme .secondary-filters-container::after {
text-decoration: none;
color: inherit;
position: relative;
width: 100%;
}
.app-item:hover {
transform: translateY(-5px);
@ -737,6 +797,7 @@ body.dark-theme .secondary-filters-container::after {
border-top: 1px solid #eee;
text-align: center;
color: #777;
width: 100%;
}
</style>
</head>
@ -783,10 +844,12 @@ body.dark-theme .secondary-filters-container::after {
</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="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>
<footer class="footer">
@ -805,10 +868,12 @@ body.dark-theme .secondary-filters-container::after {
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() {
@ -935,14 +1000,14 @@ body.dark-theme .secondary-filters-container::after {
return weightB - weightA; // 降序排列
})
);
}
});
}
});
// 渲染界面
renderFilters();
renderApps();
loadingSpinner.style.display = 'none';
primaryFilters.querySelector('[data-filter="all"]').textContent = '全部';
// 渲染界面
renderFilters();
renderApps();
loadingSpinner.style.display = 'none';
primaryFilters.querySelector('[data-filter="all"]').textContent = '全部';
} catch (error) {
console.error('加载数据失败:', error);
appListContainer.innerHTML = `
@ -951,46 +1016,49 @@ primaryFilters.querySelector('[data-filter="all"]').textContent = '全部';
</div>
`;
}
}
}
// 更新登录按钮状态
function updateLoginButton() {
if (isLoggedIn) {
// 更新登录按钮状态
function updateLoginButton() {
if (isLoggedIn) {
loginBtn.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
loginBtn.href = '/logout';
loginBtn.title = '退出登录';
adminBtn.href = '/manage'; // 已登录时直接跳转到管理页面
} else {
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;
}
// 根据分类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', () => {
// 生成筛选按钮
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);
activeSecondaryFilter = null; // 重置二级筛选状态
});
primaryFilters.appendChild(allPrimaryBtn);
// 添加一级标签按钮(从分类数据生成)
Object.entries(categories).forEach(([catId, catData]) => {
// 添加一级标签按钮(从分类数据生成)
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}`;
@ -1007,25 +1075,27 @@ Object.entries(categories).forEach(([catId, catData]) => {
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();
}
// 初始加载时显示所有二级标签
showAllSecondaryFilters();
}
// 设置所有一级标签箭头的展开/收起状态
function setAllPrimaryCaretDown(shouldDown) {
const primaryBtns = primaryFilters.querySelectorAll('[data-level="1"]');
primaryBtns.forEach(btn => {
// 设置所有一级标签箭头的展开/收起状态
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) {
@ -1036,13 +1106,13 @@ primaryBtns.forEach(btn => {
}
}
}
});
}
});
}
// 设置特定一级标签箭头的展开/收起状态
function setPrimaryCaretDown(primaryId, shouldDown) {
const btn = primaryFilters.querySelector(`[data-filter="${primaryId}"]`);
if (btn) {
// 设置特定一级标签箭头的展开/收起状态
function setPrimaryCaretDown(primaryId, shouldDown) {
const btn = primaryFilters.querySelector(`[data-filter="${primaryId}"]`);
if (btn) {
const caret = btn.querySelector('.caret');
if (caret) {
if (shouldDown) {
@ -1051,29 +1121,30 @@ if (btn) {
caret.classList.remove('down');
}
}
}
}
}
}
// 显示所有二级标签
function showAllSecondaryFilters() {
collapseAllSecondary();
// 显示所有二级标签
function showAllSecondaryFilters() {
collapseAllSecondary();
const container = document.createElement('div');
container.className = 'secondary-filters-container show';
container.id = 'secondary-all';
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', () => {
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);
activeSecondaryFilter = null; // 重置二级筛选状态
});
container.appendChild(allSecondaryBtn);
// 添加所有二级标签按钮(从分类数据生成)
Object.entries(categories).forEach(([mainCatId, mainCatData]) => {
// 添加所有二级标签按钮(从分类数据生成)
Object.entries(categories).forEach(([mainCatId, mainCatData]) => {
Object.entries(mainCatData.sub).forEach(([subCatId, subCatData]) => {
const btn = document.createElement('button');
btn.className = `filter-btn ${mainCatId}`;
@ -1083,26 +1154,33 @@ Object.entries(categories).forEach(([mainCatId, mainCatData]) => {
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;
secondaryFiltersWrapper.innerHTML = '';
secondaryFiltersWrapper.appendChild(container);
currentExpandedPrimary = null;
// 为新创建的二级筛选容器添加滚轮事件
addWheelEventToContainer(container);
}
// 为新创建的二级筛选容器添加滚轮事件
addWheelEventToContainer(container);
}
// 展开二级标签
function expandSecondary(primaryFilterId) {
collapseAllSecondary();
// 展开二级标签
function expandSecondary(primaryFilterId) {
collapseAllSecondary();
const primaryBtn = primaryFilters.querySelector(`[data-filter="${primaryFilterId}"]`);
if (primaryBtn && categories[primaryFilterId]) {
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}`;
@ -1114,6 +1192,7 @@ if (primaryBtn && categories[primaryFilterId]) {
allSecondaryBtn.dataset.filter = 'all';
allSecondaryBtn.addEventListener('click', () => {
setFilter(primaryFilterId, 'all');
activeSecondaryFilter = null; // 重置二级筛选状态
});
container.appendChild(allSecondaryBtn);
@ -1126,7 +1205,14 @@ if (primaryBtn && categories[primaryFilterId]) {
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);
});
@ -1137,61 +1223,61 @@ if (primaryBtn && categories[primaryFilterId]) {
// 为新创建的二级筛选容器添加滚轮事件
addWheelEventToContainer(container);
}
}
}
}
// 收起所有二级标签
function collapseAllSecondary() {
secondaryFiltersWrapper.innerHTML = '';
currentExpandedPrimary = null;
}
// 收起所有二级标签
function collapseAllSecondary() {
secondaryFiltersWrapper.innerHTML = '';
currentExpandedPrimary = null;
}
// 为滚动容器添加滚轮事件
function addWheelEventToContainer(container) {
container.addEventListener('wheel', (e) => {
// 为滚动容器添加滚轮事件
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;
// 设置当前筛选条件
function setFilter(primaryFilter, secondaryFilter) {
currentPrimaryFilter = primaryFilter;
currentSecondaryFilter = secondaryFilter;
// 更新按钮激活状态
document.querySelectorAll('.filter-btn').forEach(btn => {
// 更新按钮激活状态
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
});
// 激活一级标签按钮
if (primaryFilter === 'all') {
// 激活一级标签按钮
if (primaryFilter === 'all') {
primaryFilters.querySelector('[data-filter="all"]').classList.add('active');
} else {
} else {
const primaryBtn = primaryFilters.querySelector(`[data-filter="${primaryFilter}"]`);
if (primaryBtn) primaryBtn.classList.add('active');
}
}
// 激活二级标签按钮
if (secondaryFilter === 'all') {
// 激活二级标签按钮
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 {
} else {
const secondaryBtn = document.querySelector(`[data-filter="${secondaryFilter}"]`);
if (secondaryBtn) secondaryBtn.classList.add('active');
}
}
// 重新渲染应用列表
renderApps();
}
// 重新渲染应用列表
renderApps();
}
// 渲染应用列表
function renderApps() {
// 渲染应用列表
function renderApps() {
appListContainer.innerHTML = '';
// 按一级标签分组
@ -1310,34 +1396,34 @@ function renderApps() {
noResults.textContent = '没有找到匹配的应用';
appListContainer.appendChild(noResults);
}
}
}
// 辅助函数:计算对比色
function getContrastColor(hexColor) {
if (!hexColor) return '#ffffff';
// 辅助函数:计算对比色
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);
// 转换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;
// 计算亮度
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// 根据亮度返回黑色或白色
return brightness > 128 ? '#000000' : '#ffffff';
}
// 根据亮度返回黑色或白色
return brightness > 128 ? '#000000' : '#ffffff';
}
// 全局设置
let settings = {
theme: 'auto',
card_style: 'normal',
search_history: []
};
// 全局设置
let settings = {
theme: 'auto',
card_style: 'normal',
search_history: []
};
// 加载设置
async function loadSettings() {
try {
// 加载设置
async function loadSettings() {
try {
// 检查是否登录
isLoggedIn = await checkLoginStatus();
@ -1382,18 +1468,18 @@ try {
}
applySettings();
} catch (error) {
} catch (error) {
console.error('加载设置失败:', error);
}
}
}
}
// 设置背景图片
function setBackgroundImages(lightImage, darkImage) {
// 移除现有的视频背景
document.querySelectorAll('.video-bg-container').forEach(el => el.remove());
// 设置背景图片
function setBackgroundImages(lightImage, darkImage) {
// 移除现有的视频背景
document.querySelectorAll('.video-bg-container').forEach(el => el.remove());
// 处理明亮模式背景
if (lightImage && lightImage.endsWith('.mp4')) {
// 处理明亮模式背景
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';
@ -1408,12 +1494,12 @@ if (lightImage && lightImage.endsWith('.mp4')) {
lightVideoContainer.appendChild(lightVideo);
document.body.appendChild(lightVideoContainer);
} else {
} else {
document.documentElement.style.setProperty('--bg-image', lightImage === 'none' ? 'none' : `url('${lightImage}')`);
}
}
// 处理暗黑模式背景
if (darkImage && darkImage.endsWith('.mp4')) {
// 处理暗黑模式背景
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';
@ -1428,71 +1514,71 @@ if (darkImage && darkImage.endsWith('.mp4')) {
darkVideoContainer.appendChild(darkVideo);
document.body.appendChild(darkVideoContainer);
} else {
} else {
document.documentElement.style.setProperty('--dark-bg-image', darkImage === 'none' ? 'none' : `url('${darkImage}')`);
}
}
// 添加背景加载完成的类
setTimeout(() => {
// 添加背景加载完成的类
setTimeout(() => {
document.body.classList.add('bg-loaded');
}, 100);
}
}, 100);
}
// 检查登录状态
async function checkLoginStatus() {
try {
// 检查登录状态
async function checkLoginStatus() {
try {
const response = await fetch('/api/check_login');
const data = await response.json();
return data.logged_in;
} catch (error) {
} catch (error) {
console.error('检查登录状态失败:', error);
return false;
}
}
}
}
// 应用设置
function applySettings() {
// 应用主题
document.body.classList.remove('dark-theme', 'light-theme');
if (settings.theme === 'dark' ||
// 应用设置
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') {
} 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 => {
// 应用卡片样式
document.querySelectorAll('.app-item').forEach(item => {
item.classList.toggle('compact', settings.card_style === 'compact');
});
});
// 更新主题按钮状态
updateThemeButtonIcon();
}
// 更新主题按钮状态
updateThemeButtonIcon();
}
// 更新主题按钮图标
function updateThemeButtonIcon() {
const themeToggle = document.getElementById('themeToggle');
if (document.body.classList.contains('dark-theme')) {
// 更新主题按钮图标
function updateThemeButtonIcon() {
const themeToggle = document.getElementById('themeToggle');
if (document.body.classList.contains('dark-theme')) {
themeToggle.textContent = '☀️';
themeToggle.title = '切换至明亮模式';
} else {
} else {
themeToggle.textContent = '🌙';
themeToggle.title = '切换至暗黑模式';
}
}
}
}
// 保存设置
function saveSettings() {
if (isLoggedIn) {
// 保存设置
function saveSettings() {
if (isLoggedIn) {
fetch('/api/settings', {
method: 'POST',
headers: {
@ -1500,7 +1586,7 @@ if (isLoggedIn) {
},
body: JSON.stringify(settings)
});
} else {
} else {
// 游客只保存主题和卡片样式到本地
const settingsToSave = {
theme: settings.theme,
@ -1508,16 +1594,16 @@ if (isLoggedIn) {
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;
// 搜索功能
function setupSearch() {
const searchInput = document.getElementById('searchInput');
const searchResults = document.getElementById('searchResults');
let searchTimeout;
searchInput.addEventListener('input', () => {
searchInput.addEventListener('input', () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const keyword = searchInput.value.trim();
@ -1555,20 +1641,20 @@ searchInput.addEventListener('input', () => {
searchResults.style.display = 'none';
}
}, 300);
});
});
document.addEventListener('click', (e) => {
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
}
});
}
// 主题切换功能
function setupThemeSwitcher() {
const themeToggle = document.getElementById('themeToggle');
// 主题切换功能
function setupThemeSwitcher() {
const themeToggle = document.getElementById('themeToggle');
themeToggle.addEventListener('click', () => {
themeToggle.addEventListener('click', () => {
// 只在明亮和暗黑之间切换
if (document.body.classList.contains('dark-theme')) {
settings.theme = 'light';
@ -1588,22 +1674,22 @@ themeToggle.addEventListener('click', () => {
updateThemeButtonIcon();
saveSettings();
});
}
});
}
// 简洁模式切换
function setupCompactToggle() {
const compactToggle = document.getElementById('compactToggle');
compactToggle.addEventListener('click', () => {
// 简洁模式切换
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) => {
// 后台管理按钮点击事件
function setupAdminButton() {
adminBtn.addEventListener('click', async (e) => {
if (!isLoggedIn) {
e.preventDefault();
const loginCheck = await fetch('/api/check_login');
@ -1615,29 +1701,51 @@ adminBtn.addEventListener('click', async (e) => {
window.location.href = '/manage';
}
}
});
}
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
loadSettings();
setupSearch();
setupThemeSwitcher();
setupCompactToggle();
setupAdminButton();
// 返回顶部功能
function setupBackToTop() {
// 监听滚动事件
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
backToTopBtn.style.display = 'flex';
} else {
backToTopBtn.style.display = 'none';
}
});
// 为静态的一级筛选容器添加滚轮事件
primaryFilters.addEventListener('wheel', (e) => {
// 点击返回顶部按钮
backToTopBtn.addEventListener('click', () => {
// 平滑滚动到顶部
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
loadSettings();
setupSearch();
setupThemeSwitcher();
setupCompactToggle();
setupAdminButton();
setupBackToTop(); // 初始化返回顶部功能
// 为静态的一级筛选容器添加滚轮事件
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 => {
function checkScrollable() {
document.querySelectorAll('.filter-row, .secondary-filters-container').forEach(container => {
// 检查内容宽度是否大于容器宽度
const isScrollable = container.scrollWidth > container.clientWidth;
@ -1646,18 +1754,18 @@ document.querySelectorAll('.filter-row, .secondary-filters-container').forEach(c
} else {
container.classList.remove('scrollable');
}
});
}
});
}
// 初始化时检查
window.addEventListener('load', checkScrollable);
// 窗口大小改变时检查
window.addEventListener('resize', checkScrollable);
// 内容变化时检查(如筛选器更新后)
new MutationObserver(checkScrollable).observe(document.body, {
childList: true,
subtree: true
});
// 初始化时检查
window.addEventListener('load', checkScrollable);
// 窗口大小改变时检查
window.addEventListener('resize', checkScrollable);
// 内容变化时检查(如筛选器更新后)
new MutationObserver(checkScrollable).observe(document.body, {
childList: true,
subtree: true
});
</script>
</body>
</html>

View File

@ -47,6 +47,8 @@
</form>
</div>
<!-- 桌面端显示表格 -->
<div class="d-none d-md-block">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-light">
@ -93,7 +95,7 @@
<a href="{{ url_for('edit_app', index=(current_page-1)*per_page + loop.index0) }}" class="btn btn-sm btn-warning">
<i class="fas fa-edit"></i> 编辑
</a>
<a href="{{ url_for('delete_app', index=loop.index0) }}" class="btn btn-sm btn-danger" onclick="return confirm('确定删除吗?')">
<a href="{{ url_for('delete_app', index=(current_page-1)*per_page + loop.index0) }}" class="btn btn-sm btn-danger" onclick="return confirm('确定删除吗?')">
<i class="fas fa-trash"></i> 删除
</a>
</td>
@ -106,6 +108,65 @@
</tbody>
</table>
</div>
</div>
<!-- 移动端显示卡片 -->
<div class="d-md-none">
<div class="row">
{% for app in apps %}
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-3">
{% if app.icon.startswith('/upload/icon/') %}
<img src="{{ app.icon }}" class="img-fluid rounded">
{% else %}
<i class="fas {{ app.icon }} fa-2x"></i>
{% endif %}
</div>
<div class="col-9">
<h5 class="card-title mb-1">
{{ app.title }}
{% if app.get('private', False) %}
<span class="badge bg-warning text-dark ms-1">私有</span>
{% endif %}
</h5>
<div class="d-flex flex-wrap gap-1 mb-1">
<span class="badge d-flex align-items-center" style="background-color: {{ categories[app.category.main].color }}; color: white;">
{{ categories[app.category.main].name }}
<span class="badge bg-light text-dark ms-1">{{ categories[app.category.main].weight }}</span>
</span>
{% if app.category.sub %}
<span class="badge d-flex align-items-center" style="background-color: {{ categories[app.category.main].sub[app.category.sub].color }}; color: white;">
{{ categories[app.category.main].sub[app.category.sub].name }}
<span class="badge bg-light text-dark ms-1">{{ categories[app.category.main].sub[app.category.sub].weight }}</span>
</span>
{% endif %}
</div>
<p class="card-text mb-2">
<a href="{{ app.url }}" target="_blank" class="text-truncate d-block">{{ app.url }}</a>
</p>
<div class="d-flex gap-2">
<a href="{{ url_for('edit_app', index=(current_page-1)*per_page + loop.index0) }}" class="btn btn-sm btn-warning flex-grow-1">
<i class="fas fa-edit"></i> 编辑
</a>
<a href="{{ url_for('delete_app', index=(current_page-1)*per_page + loop.index0) }}" class="btn btn-sm btn-danger flex-grow-1" onclick="return confirm('确定删除吗?')">
<i class="fas fa-trash"></i> 删除
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info text-center">没有找到匹配的应用</div>
</div>
{% endfor %}
</div>
</div>
<!-- 分页导航 -->
{% if total_pages > 1 %}