first commit

This commit is contained in:
王志珏 2025-07-05 22:41:21 +08:00
commit 4672041a41
41 changed files with 5238 additions and 0 deletions

8
.idea/AIDaohang.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$APPLICATION_HOME_DIR$/jbr/bin/AIDaohang/.idea/AIDaohang.iml" filepath="$APPLICATION_HOME_DIR$/jbr/bin/AIDaohang/.idea/AIDaohang.iml" />
</modules>
</component>
</project>

33
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="bf6d7af9-34d9-40cc-9dc9-04c6564b86b3" name="变更" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectId" id="2zOBX2NNI8egQP5wk1TUn81oaXa" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="bf6d7af9-34d9-40cc-9dc9-04c6564b86b3" name="变更" comment="" />
<created>1751592275331</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1751592275331</updated>
</task>
<servers />
</component>
</project>

22
README.md Normal file
View File

@ -0,0 +1,22 @@
## 待办
1、首页标题支持自定义 - 已完成
2、每行卡片数量支持自定义4个、5个、6个、8个
3、背景图片设置支持从已上传图片中选择选择的时候提供预览 - 已完成
4、应用的图标也支持上传图片自定义
5、增加附件管理页面管理上传的图片包括应用图标图片和背景图片 - 已完成
6、首页增加页脚
7、首页悬浮气泡内的文字改为居左 - 已完成
8、一级分类和二级分类固定宽度超出宽度左右滚轮滑动查看
9、背景支持视频 - 已完成
10、应用管理页支持分页、支持选择一级分类来筛选
11、修复密码明文传输 - 已完成
12、不登录时一级分类和二级分类按钮没有颜色 - 已完成
13、部分页面游客跳转登录 - 已完成
14、私有应用在首页也要有标识;
15、气泡的小箭头靠左
16、logo图标的设置也区分明亮和暗黑;
17、应用分页、附件分页
18、应用批量选择批量删除批量私有化、公有化
19、附件批量选择、批量删除
20、网站图标自动获取
21、书签收藏工具

834
app.py Normal file
View File

@ -0,0 +1,834 @@
import os
import json
import datetime
import random
import uuid
from flask import Flask, render_template, request, redirect, url_for, jsonify, session, make_response, flash, send_from_directory
from werkzeug.security import generate_password_hash, check_password_hash
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from functools import wraps
from werkzeug.utils import secure_filename
import requests
from urllib.parse import urlparse
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # 请更改为安全的密钥
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, 'data')
# 确保数据目录存在
os.makedirs(DATA_DIR, exist_ok=True)
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'upload')
LOGO_FOLDER = os.path.join(UPLOAD_FOLDER, 'logo')
BACKGROUND_FOLDER = os.path.join(UPLOAD_FOLDER, 'background')
os.makedirs(LOGO_FOLDER, exist_ok=True)
os.makedirs(BACKGROUND_FOLDER, exist_ok=True)
VIDEO_FOLDER = os.path.join(UPLOAD_FOLDER, 'video')
os.makedirs(VIDEO_FOLDER, exist_ok=True)
# 允许的文件扩展名
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'mp4'}
ATTACHMENTS_FILE = os.path.join(DATA_DIR, 'attachments.json')
# 系统设置文件路径
SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
def migrate_settings(settings):
"""迁移旧版设置到新版格式"""
if 'admin_password' in settings:
# 如果存在旧版明文密码,转换为哈希
settings['admin_password_hash'] = generate_password_hash(settings['admin_password'])
del settings['admin_password']
elif 'admin_password_hash' not in settings:
# 如果既没有旧版密码也没有哈希,生成默认密码哈希
settings['admin_password_hash'] = generate_password_hash('123456')
return settings
def init_settings():
"""初始化或迁移设置文件"""
if not os.path.exists(SETTINGS_FILE):
# 全新安装的默认设置
default_settings = {
"theme": "auto",
"card_style": "normal",
"bg_image": "none",
"dark_bg_image": "none",
"admin_password_hash": generate_password_hash("123456"),
"site_title": "应用导航中心",
"show_logo": True,
"logo_type": "icon",
"logo_icon": "fa-th-list",
"logo_image": "",
"uploaded_backgrounds": [],
"uploaded_logos": []
}
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(default_settings, f, ensure_ascii=False, indent=2)
else:
# 已有设置文件,检查是否需要迁移
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
settings = json.load(f)
if 'admin_password_hash' not in settings:
# 需要迁移
settings = migrate_settings(settings)
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
init_settings()
def load_settings():
"""加载设置并确保包含密码哈希"""
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
settings = json.load(f)
# 双重检查,确保始终有密码哈希
if 'admin_password_hash' not in settings:
settings = migrate_settings(settings)
save_settings(settings)
return settings
def save_settings(data):
"""保存设置,确保不存储明文密码"""
if 'admin_password' in data:
del data['admin_password']
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load_apps():
with open(os.path.join(DATA_DIR, 'apps-data.json'), 'r', encoding='utf-8') as f:
return json.load(f)
def save_apps(data):
with open(os.path.join(DATA_DIR, 'apps-data.json'), 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load_categories():
with open(os.path.join(DATA_DIR, 'categories.json'), 'r', encoding='utf-8') as f:
categories = json.load(f)
# 确保每个子分类都有weight属性
for main_cat in categories.values():
if 'sub' in main_cat:
for sub_cat in main_cat['sub'].values():
if 'weight' not in sub_cat:
sub_cat['weight'] = 0
if 'weight' not in main_cat:
main_cat['weight'] = 0
return categories
def load_icons():
with open(os.path.join(BASE_DIR, 'data', 'icons.json'), 'r', encoding='utf-8') as f:
return json.load(f)
def save_categories(data):
with open(os.path.join(DATA_DIR, 'categories.json'), 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session:
flash('请先登录', 'danger')
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
def init_attachments():
if not os.path.exists(ATTACHMENTS_FILE):
with open(ATTACHMENTS_FILE, 'w', encoding='utf-8') as f:
json.dump([], f, ensure_ascii=False, indent=2)
init_attachments()
def load_attachments():
with open(ATTACHMENTS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def save_attachments(data):
with open(ATTACHMENTS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def add_attachment(filename, file_type):
attachments = load_attachments()
attachments.append({
'filename': filename,
'type': file_type,
'upload_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
save_attachments(attachments)
def delete_attachment(filename):
attachments = load_attachments()
attachments = [a for a in attachments if a['filename'] != filename]
save_attachments(attachments)
# 从settings中移除引用
settings = load_settings()
if settings.get('bg_image', '').endswith(filename):
settings['bg_image'] = ""
if settings.get('dark_bg_image', '').endswith(filename):
settings['dark_bg_image'] = ""
if settings.get('logo_image', '').endswith(filename):
settings['logo_image'] = ""
save_settings(settings)
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def generate_random_filename(extension):
return f"{uuid.uuid4().hex}.{extension}"
def download_image(url, folder):
try:
response = requests.get(url, stream=True)
if response.status_code == 200:
# 获取文件扩展名
content_type = response.headers.get('content-type', '')
if 'image/jpeg' in content_type:
ext = 'jpg'
elif 'image/png' in content_type:
ext = 'png'
elif 'image/gif' in content_type:
ext = 'gif'
else:
ext = 'jpg' # 默认扩展名
filename = generate_random_filename(ext)
filepath = os.path.join(folder, filename)
with open(filepath, 'wb') as f:
for chunk in response.iter_content(1024):
f.write(chunk)
return filename
except Exception as e:
print(f"Error downloading image: {e}")
return None
@app.route('/api/check_login')
def check_login():
return jsonify({'logged_in': 'username' in session})
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
captcha = request.form['captcha']
if 'captcha' not in session or captcha.lower() != session['captcha'].lower():
flash('验证码错误', 'danger')
return render_template('login.html')
settings = load_settings()
if username == 'admin' and 'admin_password_hash' in settings:
if check_password_hash(settings['admin_password_hash'], password):
session['username'] = username
next_url = request.args.get('next', url_for('index'))
flash('登录成功', 'success')
return redirect(next_url)
flash('用户名或密码错误', 'danger')
return render_template('login.html')
return render_template('login.html')
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('navigation'))
@app.route('/captcha')
def captcha():
# 生成随机4位验证码
captcha_text = ''.join(random.choices('abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789', k=4))
session['captcha'] = captcha_text
# 创建图片
image = Image.new('RGB', (120, 40), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
# 使用默认字体或指定字体路径
try:
font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
# 绘制验证码文本
draw.text((10, 5), captcha_text, font=font, fill=(0, 0, 0))
# 添加干扰线
for i in range(5):
x1 = random.randint(0, 120)
y1 = random.randint(0, 40)
x2 = random.randint(0, 120)
y2 = random.randint(0, 40)
draw.line((x1, y1, x2, y2), fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))
# 保存图片到内存
img_io = BytesIO()
image.save(img_io, 'PNG')
img_io.seek(0)
response = make_response(img_io.getvalue())
response.headers['Content-Type'] = 'image/png'
return response
@app.route('/settings', methods=['GET', 'POST'])
@login_required
def system_settings():
settings = load_settings()
attachments = load_attachments()
if request.method == 'POST':
try:
# 更新基本设置
settings['theme'] = request.form.get('theme', 'auto')
settings['card_style'] = request.form.get('card_style', 'normal')
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')
# 处理logo设置
if settings['logo_type'] == 'icon':
settings['logo_icon'] = request.form.get('logo_icon', 'fa-th-list')
else:
logo_image_type = request.form.get('logo_image_type')
if logo_image_type == 'existing':
existing_logo = request.form.get('selected_logo')
if existing_logo:
settings['logo_image'] = f"/upload/logo/{existing_logo}"
elif logo_image_type == 'none':
settings['logo_image'] = ""
# 处理明亮模式背景
bg_type = request.form.get('bg_type')
if bg_type == 'existing':
existing_bg = request.form.get('selected_bg')
if existing_bg:
settings['bg_image'] = f"/upload/background/{existing_bg}"
else:
settings['bg_image'] = "none"
elif bg_type == 'video':
existing_video = request.form.get('selected_video')
if existing_video:
settings['bg_image'] = f"/upload/video/{existing_video}"
else:
settings['bg_image'] = "none"
elif bg_type == 'default':
settings['bg_image'] = "/static/background_light.jpg"
elif bg_type == 'none':
settings['bg_image'] = "none"
# 处理暗黑模式背景
dark_bg_type = request.form.get('dark_bg_type')
if dark_bg_type == 'existing':
existing_dark_bg = request.form.get('selected_dark_bg')
if existing_dark_bg:
settings['dark_bg_image'] = f"/upload/background/{existing_dark_bg}"
else:
settings['dark_bg_image'] = "none"
elif dark_bg_type == 'video':
existing_dark_video = request.form.get('selected_dark_video')
if existing_dark_video:
settings['dark_bg_image'] = f"/upload/video/{existing_dark_video}"
else:
settings['dark_bg_image'] = "none"
elif dark_bg_type == 'default':
settings['dark_bg_image'] = "/static/background_dark.jpg"
elif dark_bg_type == 'none':
settings['dark_bg_image'] = "none"
# 检查是否修改密码
old_password = request.form.get('old_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
if old_password and new_password and confirm_password:
if not check_password_hash(settings.get('admin_password_hash', ''), old_password):
flash('原密码错误', 'danger')
return render_template('settings.html', settings=settings, icons=load_icons(), attachments=attachments)
if new_password != confirm_password:
flash('新密码不匹配', 'danger')
return render_template('settings.html', settings=settings, icons=load_icons(), attachments=attachments)
settings['admin_password_hash'] = generate_password_hash(new_password)
save_settings(settings)
flash('设置已保存', 'success')
return redirect(url_for('system_settings'))
except Exception as e:
print(f"保存设置时出错: {e}")
flash('保存设置时出错', 'danger')
return render_template('settings.html', settings=settings, icons=load_icons(), attachments=attachments)
# 从返回的settings中移除密码哈希
settings_to_return = settings.copy()
settings_to_return.pop('admin_password_hash', None)
return render_template('settings.html', settings=settings_to_return, icons=load_icons(), attachments=attachments)
# 添加附件管理路由
@app.route('/attachments')
@login_required
def manage_attachments():
settings = load_settings()
attachments = load_attachments()
return render_template('attachments.html', settings=settings, attachments=attachments)
@app.route('/upload_attachment', methods=['POST'])
@login_required
def upload_attachment():
if 'file' not in request.files:
flash('没有选择文件', 'danger')
return redirect(url_for('manage_attachments'))
file = request.files['file']
file_type = request.form.get('type')
if file.filename == '':
flash('没有选择文件', 'danger')
return redirect(url_for('manage_attachments'))
if file and allowed_file(file.filename):
ext = file.filename.rsplit('.', 1)[1].lower()
filename = generate_random_filename(ext)
if file_type == 'logo':
folder = LOGO_FOLDER
elif file_type == 'background':
folder = BACKGROUND_FOLDER
else: # video
folder = VIDEO_FOLDER
filepath = os.path.join(folder, filename)
file.save(filepath)
add_attachment(filename, file_type)
flash('文件上传成功', 'success')
else:
flash('不允许的文件类型', 'danger')
return redirect(url_for('manage_attachments'))
# 添加视频路由
@app.route('/upload/video/<filename>')
def uploaded_video(filename):
return send_from_directory(VIDEO_FOLDER, filename)
@app.route('/delete_attachment/<filename>', methods=['POST'])
@login_required
def delete_attachment_route(filename):
try:
# 检查文件是否存在
file_found = False
for folder in [LOGO_FOLDER, BACKGROUND_FOLDER]:
filepath = os.path.join(folder, filename)
if os.path.exists(filepath):
os.remove(filepath)
file_found = True
if file_found:
delete_attachment(filename)
flash('附件删除成功', 'success')
else:
flash('文件不存在', 'danger')
except Exception as e:
print(f"删除附件时出错: {e}")
flash('删除附件时出错', 'danger')
return redirect(url_for('manage_attachments'))
@app.route('/upload/background/<filename>')
def uploaded_background(filename):
return send_from_directory(BACKGROUND_FOLDER, filename)
@app.route('/upload/logo/<filename>')
def uploaded_logo(filename):
return send_from_directory(LOGO_FOLDER, filename)
@app.route('/api/settings', methods=['GET', 'POST'])
@login_required
def handle_settings():
settings = load_settings()
if request.method == 'POST':
data = request.json
# 确保不通过API修改密码哈希
if 'admin_password_hash' in data:
del data['admin_password_hash']
settings.update(data)
save_settings(settings)
return jsonify({'status': 'success'})
else:
# 从API响应中移除密码哈希
settings_to_return = settings.copy()
settings_to_return.pop('admin_password_hash', None)
return jsonify(settings_to_return)
@app.route('/manage')
@login_required
def index():
category_filter = request.args.get('category')
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
app['category']['sub'] == category_filter or
(category_filter in categories and app['category']['main'] == category_filter)):
filtered_apps.append(app)
apps = filtered_apps
return render_template('manage.html', apps=apps, settings=settings, categories=categories)
@app.route('/')
def navigation():
settings = load_settings()
return render_template('index.html',
bg_image=settings.get('bg_image', ''),
dark_bg_image=settings.get('dark_bg_image', ''),
is_logged_in='username' in session,
settings=settings) # 添加settings到模板上下文
@app.route('/api/search', methods=['POST'])
def search_apps():
keyword = request.json.get('keyword', '').lower()
apps = load_apps()
results = []
# 检查登录状态
is_logged_in = 'username' in session
for app in apps:
# 如果是私有应用且未登录,则跳过
if app.get('private', False) and not is_logged_in:
continue
if (keyword in app['title'].lower() or
keyword in app['url'].lower() or
keyword in app.get('description', '').lower() or
keyword in app['category']['main'].lower() or
keyword in app['category']['sub'].lower()):
results.append(app)
return jsonify(results)
@app.route('/app/add', methods=['GET', 'POST'])
@login_required
def add_app():
categories = load_categories()
icons = load_icons()
settings = load_settings()
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)
return redirect(url_for('index'))
return render_template('add_app.html', categories=categories, settings=settings, icons=icons)
@app.route('/app/edit/<int:index>', methods=['GET', 'POST'])
@login_required
def edit_app(index):
categories = load_categories()
apps = load_apps()
icons = load_icons()
settings = load_settings()
if request.method == 'POST':
apps[index]['title'] = request.form['title']
apps[index]['url'] = request.form['url']
apps[index]['icon'] = request.form['icon']
apps[index]['description'] = request.form.get('description', '')
apps[index]['private'] = 'private' in request.form
apps[index]['category'] = {
"main": request.form['main_category'],
"sub": request.form['sub_category']
}
save_apps(apps)
return redirect(url_for('index'))
return render_template('edit_app.html', app=apps[index], index=index, settings=settings, categories=categories, icons=icons)
@app.route('/api/icons')
def get_icons():
icons = load_icons()
return jsonify(icons)
@app.route('/app/delete/<int:index>')
@login_required
def delete_app(index):
apps = load_apps()
apps.pop(index)
save_apps(apps)
return redirect(url_for('index'))
@app.route('/categories')
@login_required
def manage_categories():
categories = load_categories()
icons = load_icons()
settings = load_settings()
return render_template('categories.html', categories=categories, settings=settings, icons=icons)
@app.route('/categories/add_main', methods=['POST'])
@login_required
def add_main_category():
categories = load_categories()
cat_id = request.form['id']
cat_name = request.form['name']
cat_color = request.form.get('color', '#4361ee')
is_private = 'private' in request.form
weight = int(request.form.get('weight', 0))
categories[cat_id] = {
"name": cat_name,
"color": cat_color,
"sub": {},
"private": is_private,
"sub_private": {},
"weight": weight
}
save_categories(categories)
return redirect(url_for('manage_categories'))
@app.route('/categories/add_sub', methods=['POST'])
@login_required
def add_sub_category():
categories = load_categories()
main_id = request.form['main_id']
sub_id = request.form['sub_id']
sub_name = request.form['sub_name']
sub_color = request.form.get('color', '#4895ef')
is_private = 'private' in request.form
weight = int(request.form.get('weight', 0))
if main_id not in categories:
flash('主分类不存在', 'danger')
return redirect(url_for('manage_categories'))
categories[main_id]['sub'][sub_id] = {
"name": sub_name,
"color": sub_color,
"weight": weight
}
if is_private:
if 'sub_private' not in categories[main_id]:
categories[main_id]['sub_private'] = {}
categories[main_id]['sub_private'][sub_id] = True
save_categories(categories)
return redirect(url_for('manage_categories'))
@app.route('/categories/delete_main/<string:main_id>')
@login_required
def delete_main_category(main_id):
categories = load_categories()
if main_id in categories:
categories.pop(main_id)
save_categories(categories)
# 删除相关应用
apps = load_apps()
apps = [app for app in apps if app['category']['main'] != main_id]
save_apps(apps)
return redirect(url_for('manage_categories'))
@app.route('/categories/delete_sub/<string:main_id>/<string:sub_id>')
@login_required
def delete_sub_category(main_id, sub_id):
categories = load_categories()
if main_id in categories and sub_id in categories[main_id].get('sub', {}):
categories[main_id]['sub'].pop(sub_id)
# 同时删除私有标记
if 'sub_private' in categories[main_id] and sub_id in categories[main_id]['sub_private']:
categories[main_id]['sub_private'].pop(sub_id)
save_categories(categories)
# 更新相关应用
apps = load_apps()
for app in apps:
if app['category']['main'] == main_id and app['category']['sub'] == sub_id:
app['category']['sub'] = ""
save_apps(apps)
return redirect(url_for('manage_categories'))
@app.route('/get_subcategories/<string:main_id>')
def get_subcategories(main_id):
categories = load_categories()
if main_id in categories:
subcategories = {}
for sub_id, sub_data in categories[main_id].get('sub', {}).items():
subcategories[sub_id] = sub_data['name'] if isinstance(sub_data, dict) else sub_data
return jsonify(subcategories)
return jsonify({})
@app.route('/categories/edit_main/<string:main_id>', methods=['GET', 'POST'])
@login_required
def edit_main_category(main_id):
categories = load_categories()
settings = load_settings()
if main_id not in categories:
return redirect(url_for('manage_categories'))
if request.method == 'POST':
categories[main_id]['name'] = request.form['name']
categories[main_id]['color'] = request.form.get('color', '#4361ee')
categories[main_id]['private'] = 'private' in request.form
categories[main_id]['weight'] = int(request.form.get('weight', 0))
save_categories(categories)
return redirect(url_for('manage_categories'))
return render_template('edit_main_category.html',
category=categories[main_id],
settings=settings,
main_id=main_id)
@app.route('/categories/edit_sub/<string:main_id>/<string:sub_id>', methods=['GET', 'POST'])
@login_required
def edit_sub_category(main_id, sub_id):
categories = load_categories()
settings = load_settings()
if main_id not in categories or sub_id not in categories[main_id]['sub']:
return redirect(url_for('manage_categories'))
if request.method == 'POST':
new_sub_id = request.form['sub_id']
new_sub_name = request.form['sub_name']
new_color = request.form.get('color', '#4895ef')
is_private = 'private' in request.form
weight = int(request.form.get('weight', 0))
# 如果ID改变了需要先删除旧的
if sub_id != new_sub_id:
del categories[main_id]['sub'][sub_id]
# 更新或添加新的子分类
categories[main_id]['sub'][new_sub_id] = {
"name": new_sub_name,
"color": new_color,
"weight": weight
}
if 'sub_private' not in categories[main_id]:
categories[main_id]['sub_private'] = {}
categories[main_id]['sub_private'][new_sub_id] = is_private
save_categories(categories)
return redirect(url_for('manage_categories'))
is_private = categories[main_id].get('sub_private', {}).get(sub_id, False)
sub_data = categories[main_id]['sub'][sub_id]
return render_template('edit_sub_category.html',
main_id=main_id,
sub_id=sub_id,
sub_name=sub_data['name'],
color=sub_data['color'],
weight=sub_data.get('weight', 0),
settings=settings,
is_private=is_private)
@app.route('/api/apps')
def api_apps():
apps = load_apps()
# 检查登录状态
is_logged_in = 'username' in session
# 如果未登录,过滤掉私有应用
if not is_logged_in:
apps = [app for app in apps if not app.get('private', False)]
return jsonify(apps)
@app.route('/api/categories')
def api_categories():
categories = load_categories()
# 检查登录状态
is_logged_in = 'username' in session
# 按权重排序主分类
sorted_categories = dict(sorted(categories.items(),
key=lambda x: x[1].get('weight', 0),
reverse=True))
# 如果未登录,过滤掉私有分类但保留颜色信息
if not is_logged_in:
filtered_categories = {}
for main_id, main_data in sorted_categories.items():
if not main_data.get('private', False):
filtered_sub = {}
# 按权重排序子分类
sorted_sub = dict(sorted(main_data['sub'].items(),
key=lambda x: x[1].get('weight', 0),
reverse=True))
for sub_id, sub_data in sorted_sub.items():
if not main_data.get('sub_private', {}).get(sub_id, False):
filtered_sub[sub_id] = sub_data
if filtered_sub or main_data.get('sub', {}): # 只添加有子分类的主分类
filtered_categories[main_id] = {
'name': main_data['name'],
'color': main_data['color'], # 保留颜色信息
'sub': filtered_sub
}
return jsonify(filtered_categories)
# 对子分类也按权重排序
for main_id, main_data in sorted_categories.items():
main_data['sub'] = dict(sorted(main_data['sub'].items(),
key=lambda x: x[1].get('weight', 0),
reverse=True))
return jsonify(sorted_categories)
if __name__ == '__main__':
app.run(debug=True)

113
data/apps-data.json Normal file
View File

@ -0,0 +1,113 @@
[
{
"title": "SQL生成工具23",
"url": "https://fastai.liuyan.wang/chat/share?shareId=dbvns61a0glb2q6nwv9utd5v",
"icon": "fa-solid fa-drumstick-bite",
"description": "一个强大的SQL查询生成工具支持多种数据库",
"category": {
"main": "dev",
"sub": "data"
},
"private": true
},
{
"title": "Nginx配置生成",
"url": "https://fastai.liuyan.wang/chat/share?shareId=3340xwddpp3xi48g1evv7r4m",
"icon": "fa-server",
"description": "快速生成Nginx配置文件",
"category": {
"main": "dev",
"sub": "web"
}
},
{
"title": "动物的一生",
"url": "https://fastai.liuyan.wang/chat/share?shareId=0e5kd8ged5w7oxrhfcztvt5b",
"icon": "fa-paw",
"description": "了解各种动物的生命周期",
"category": {
"main": "edu",
"sub": "science"
}
},
{
"title": "Docker转换工具",
"url": "https://fastai.liuyan.wang/chat/share?shareId=9ylu7kn735vc7z69n3otryss",
"icon": "fa-docker",
"description": "将不同格式的容器配置相互转换",
"category": {
"main": "dev",
"sub": "ops"
}
},
{
"title": "日报工具",
"url": "https://fastai.liuyan.wang/chat/share?shareId=arylhdiwlr84qmzmublhlbjj",
"icon": "fa-calendar-day",
"description": "123",
"category": {
"main": "tool",
"sub": "office"
}
},
{
"title": "NFS-PV-PVC",
"url": "https://fastai.liuyan.wang/chat/share?shareId=szkw0erj0eukvu8uch2kznzm",
"icon": "fa-network-wired",
"description": "123",
"category": {
"main": "dev",
"sub": "cloud"
}
},
{
"title": "刑法助手",
"url": "https://fastai.liuyan.wang/chat/share?shareId=6ot25ul3kli322rybodpring",
"icon": "fa-gavel",
"description": "123",
"category": {
"main": "law",
"sub": "criminal"
}
},
{
"title": "税法助手",
"url": "https://fastai.liuyan.wang/chat/share?shareId=rjazldnzqc1gr03k1giije7v",
"icon": "fa-file-invoice-dollar",
"description": "123",
"category": {
"main": "law",
"sub": "tax"
}
},
{
"title": "汉语新解",
"url": "https://fastai.liuyan.wang/chat/share?shareId=lif7r64uc12rmkhgv51gxslh",
"icon": "fa-language",
"description": "123",
"category": {
"main": "edu",
"sub": "language"
}
},
{
"title": "AI绘画提示词",
"url": "https://fastai.liuyan.wang/chat/share?shareId=example1",
"icon": "fa-paint-brush",
"category": {
"main": "ai",
"sub": "image"
},
"description": "",
"private": false
},
{
"title": "AI写作助手",
"url": "https://fastai.liuyan.wang/chat/share?shareId=example2",
"icon": "fa-keyboard",
"category": {
"main": "ai",
"sub": "writing"
}
}
]

37
data/attachments.json Normal file
View File

@ -0,0 +1,37 @@
[
{
"filename": "d078c01de3be46deab9e85a94285d785.png",
"type": "background",
"upload_time": "2025-07-05 13:55:10"
},
{
"filename": "5dd4f5d3cd7b48eca9967fa063ea5cd9.png",
"type": "background",
"upload_time": "2025-07-05 13:55:15"
},
{
"filename": "b2c128cf2d4e47daa349c5e7f38c932c.png",
"type": "logo",
"upload_time": "2025-07-05 13:58:32"
},
{
"filename": "5378dda810964da9a7515ec844628738.png",
"type": "logo",
"upload_time": "2025-07-05 16:46:09"
},
{
"filename": "f40e2eb965b24e358a5bba9523231f8f.png",
"type": "logo",
"upload_time": "2025-07-05 16:46:16"
},
{
"filename": "e4e762f039ce471489fc65db6cd395c7.mp4",
"type": "video",
"upload_time": "2025-07-05 20:54:08"
},
{
"filename": "b23249a9681840329afb0c4af489fc30.mp4",
"type": "video",
"upload_time": "2025-07-05 21:02:17"
}
]

104
data/categories.json Normal file
View File

@ -0,0 +1,104 @@
{
"dev": {
"name": "开发工具",
"color": "#4cc9f0",
"sub": {
"data": {
"name": "数据库",
"color": "#4895ef",
"weight": 3
},
"web": {
"name": "Web服务",
"color": "#4361ee",
"weight": 5
},
"ops": {
"name": "运维部署",
"color": "#3a0ca3",
"weight": 2
},
"cloud": {
"name": "云计算",
"color": "#480ca8",
"weight": 0
}
},
"private": true,
"sub_private": {
"data": true,
"web": true,
"ops": true,
"cloud": true
},
"weight": 99999
},
"edu": {
"name": "教育学习",
"color": "#f72585",
"sub": {
"science": {
"name": "自然科学",
"color": "#b5179e",
"weight": 0
},
"language": {
"name": "语言",
"color": "#7209b7",
"weight": 0
}
},
"private": false,
"weight": 0
},
"tool": {
"name": "效率工具",
"color": "#7209b7",
"sub": {
"office": {
"name": "办公",
"color": "#560bad",
"weight": 0
}
},
"weight": 0
},
"law": {
"name": "法律相关",
"color": "#4895ef",
"sub": {
"criminal": {
"name": "刑法",
"color": "#4361ee",
"weight": 0
},
"tax": {
"name": "税法",
"color": "#3f37c9",
"weight": 0
}
},
"weight": 0
},
"ai": {
"name": "AI工具",
"color": "#f8961e",
"sub": {
"image": {
"name": "图像生成",
"color": "#f3722c",
"weight": 999
},
"writing": {
"name": "写作",
"color": "#f9844a",
"weight": 0
}
},
"private": false,
"sub_private": {
"image": false
},
"weight": 29999
}
}

922
data/icons.json Normal file
View File

@ -0,0 +1,922 @@
{
"fa-solid fa-house": "房屋",
"fa-solid fa-code": "代码",
"fa-solid fa-database": "数据库",
"fa-solid fa-server": "服务器",
"fa-solid fa-cloud": "云",
"fa-solid fa-laptop-code": "编程",
"fa-solid fa-book": "书籍",
"fa-solid fa-graduation-cap": "教育",
"fa-solid fa-flask": "科学",
"fa-solid fa-language": "语言",
"fa-solid fa-file-word": "文档",
"fa-solid fa-file-excel": "表格",
"fa-solid fa-file-powerpoint": "演示",
"fa-solid fa-gavel": "法律",
"fa-solid fa-balance-scale": "平衡",
"fa-solid fa-robot": "AI",
"fa-solid fa-brain": "智能",
"fa-solid fa-image": "图像",
"fa-solid fa-paint-brush": "绘画",
"fa-solid fa-search": "搜索",
"fa-solid fa-link": "链接",
"fa-solid fa-globe": "地球",
"fa-solid fa-envelope": "邮件",
"fa-solid fa-calendar": "日历",
"fa-solid fa-clock": "时钟",
"fa-solid fa-calculator": "计算器",
"fa-solid fa-chart-line": "图表",
"fa-solid fa-users": "用户",
"fa-solid fa-shield-alt": "安全",
"fa-solid fa-lock": "锁",
"fa-solid fa-key": "钥匙",
"fa-solid fa-terminal": "终端",
"fa-solid fa-network-wired": "网络",
"fa-solid fa-desktop": "桌面",
"fa-solid fa-mobile-alt": "手机",
"fa-solid fa-tablet-alt": "平板",
"fa-solid fa-print": "打印",
"fa-solid fa-camera": "相机",
"fa-solid fa-video": "视频",
"fa-solid fa-microphone": "麦克风",
"fa-solid fa-music": "音乐",
"fa-solid fa-gamepad": "游戏",
"fa-solid fa-puzzle-piece": "拼图",
"fa-solid fa-map": "地图",
"fa-solid fa-compass": "指南针",
"fa-solid fa-flag": "旗帜",
"fa-solid fa-heart": "心形",
"fa-solid fa-star": "星星",
"fa-solid fa-bell": "铃铛",
"fa-solid fa-gift": "礼物",
"fa-solid fa-shopping-cart": "购物车",
"fa-solid fa-credit-card": "信用卡",
"fa-solid fa-money-bill-wave": "货币",
"fa-solid fa-coins": "硬币",
"fa-solid fa-piggy-bank": "存钱罐",
"fa-solid fa-briefcase": "公文包",
"fa-solid fa-suitcase": "行李箱",
"fa-solid fa-building": "建筑",
"fa-solid fa-hospital": "医院",
"fa-solid fa-ambulance": "救护车",
"fa-solid fa-fire": "火",
"fa-solid fa-leaf": "叶子",
"fa-solid fa-tree": "树",
"fa-solid fa-seedling": "幼苗",
"fa-solid fa-recycle": "回收",
"fa-solid fa-sun": "太阳",
"fa-solid fa-moon": "月亮",
"fa-solid fa-cloud-sun": "多云",
"fa-solid fa-cloud-rain": "雨",
"fa-solid fa-snowflake": "雪花",
"fa-solid fa-wind": "风",
"fa-solid fa-umbrella": "雨伞",
"fa-solid fa-bolt": "闪电",
"fa-solid fa-fire-extinguisher": "灭火器",
"fa-solid fa-shower": "淋浴",
"fa-solid fa-bath": "浴缸",
"fa-solid fa-toilet": "马桶",
"fa-solid fa-couch": "沙发",
"fa-solid fa-bed": "床",
"fa-solid fa-chair": "椅子",
"fa-solid fa-utensils": "餐具",
"fa-solid fa-mug-hot": "杯子",
"fa-solid fa-blender": "搅拌机",
"fa-solid fa-ice-cream": "冰淇淋",
"fa-solid fa-hamburger": "汉堡",
"fa-solid fa-pizza-slice": "披萨",
"fa-solid fa-b bread-slice": "面包",
"fa-solid fa-cheese": "奶酪",
"fa-solid fa-egg": "鸡蛋",
"fa-solid fa-apple-alt": "苹果",
"fa-solid fa-lemon": "柠檬",
"fa-solid fa-pepper-hot": "辣椒",
"fa-solid fa-fish": "鱼",
"fa-solid fa-drumstick-bite": "鸡腿",
"fa-solid fa-carrot": "胡萝卜",
"fa-solid fa-beer": "啤酒",
"fa-solid fa-wine-glass-alt": "酒杯",
"fa-solid fa-cocktail": "鸡尾酒",
"fa-solid fa-coffee": "咖啡",
"fa-solid fa-tea": "茶",
"fa-solid fa-user": "用户",
"fa-solid fa-users-cog": "管理员",
"fa-solid fa-tools": "工具",
"fa-solid fa-cog": "设置",
"fa-solid fa-sync": "同步",
"fa-solid fa-download": "下载",
"fa-solid fa-upload": "上传",
"fa-solid fa-trash": "删除",
"fa-solid fa-edit": "编辑",
"fa-solid fa-save": "保存",
"fa-solid fa-folder": "文件夹",
"fa-solid fa-folder-open": "打开文件夹",
"fa-solid fa-file": "文件",
"fa-solid fa-file-alt": "文本文件",
"fa-solid fa-file-pdf": "PDF",
"fa-solid fa-file-image": "图片文件",
"fa-solid fa-file-audio": "音频文件",
"fa-solid fa-file-video": "视频文件",
"fa-solid fa-file-archive": "压缩文件",
"fa-solid fa-file-code": "代码文件",
"fa-solid fa-barcode": "条形码",
"fa-solid fa-qrcode": "二维码",
"fa-solid fa-tag": "标签",
"fa-solid fa-tags": "多个标签",
"fa-solid fa-comment": "评论",
"fa-solid fa-comments": "多条评论",
"fa-solid fa-thumbs-up": "点赞",
"fa-solid fa-thumbs-down": "点踩",
"fa-solid fa-share": "分享",
"fa-solid fa-external-link-alt": "外部链接",
"fa-solid fa-eye": "查看",
"fa-solid fa-eye-slash": "隐藏",
"fa-solid fa-filter": "筛选",
"fa-solid fa-sort": "排序",
"fa-solid fa-arrow-up": "向上箭头",
"fa-solid fa-arrow-down": "向下箭头",
"fa-solid fa-arrow-left": "向左箭头",
"fa-solid fa-arrow-right": "向右箭头",
"fa-solid fa-expand": "放大",
"fa-solid fa-compress": "缩小",
"fa-solid fa-plus": "加号",
"fa-solid fa-minus": "减号",
"fa-solid fa-times": "关闭",
"fa-solid fa-check": "确认",
"fa-solid fa-ban": "禁止",
"fa-solid fa-info-circle": "信息",
"fa-solid fa-exclamation-triangle": "警告",
"fa-solid fa-exclamation-circle": "错误",
"fa-solid fa-question-circle": "帮助",
"fa-solid fa-check-circle": "成功",
"fa-solid fa-ellipsis-h": "更多",
"fa-solid fa-headphones": "耳机",
"fa-solid fa-tv": "电视",
"fa-solid fa-gamepad": "游戏手柄",
"fa-solid fa-dumbbell": "健身",
"fa-solid fa-bicycle": "自行车",
"fa-solid fa-bus": "公交车",
"fa-solid fa-car": "汽车",
"fa-solid fa-plane": "飞机",
"fa-solid fa-ship": "轮船",
"fa-solid fa-train": "火车",
"fa-solid fa-subway": "地铁",
"fa-solid fa-motorcycle": "摩托车",
"fa-solid fa-paper-plane": "纸飞机",
"fa-solid fa-space-shuttle": "航天飞机",
"fa-solid fa-satellite": "卫星",
"fa-solid fa-microscope": "显微镜",
"fa-solid fa-atom": "原子",
"fa-solid fa-vial": "试管",
"fa-solid fa-prescription-bottle": "药瓶",
"fa-solid fa-stethoscope": "听诊器",
"fa-solid fa-syringe": "注射器",
"fa-solid fa-dna": "DNA",
"fa-solid fa-wheelchair": "轮椅",
"fa-solid fa-crutch": "拐杖",
"fa-solid fa-procedures": "医疗程序",
"fa-solid fa-first-aid": "急救",
"fa-solid fa-hand-holding-heart": "手捧爱心",
"fa-solid fa-handshake": "握手",
"fa-solid fa-praying-hands": "祈祷",
"fa-solid fa-church": "教堂",
"fa-solid fa-mosque": "清真寺",
"fa-solid fa-synagogue": "犹太教堂",
"fa-solid fa-kaaba": "天房",
"fa-solid fa-torah": "托拉",
"fa-solid fa-bible": "圣经",
"fa-solid fa-quran": "古兰经",
"fa-solid fa-place-of-worship": "礼拜场所",
"fa-solid fa-angry": "生气",
"fa-solid fa-dizzy": "晕眩",
"fa-solid fa-flushed": "脸红",
"fa-solid fa-frown": "皱眉",
"fa-solid fa-frown-open": "张嘴皱眉",
"fa-solid fa-grimace": "鬼脸",
"fa-solid fa-grin": "露齿笑",
"fa-solid fa-grin-alt": "咧嘴笑",
"fa-solid fa-grin-beam": "开心笑",
"fa-solid fa-grin-beam-sweat": "尴尬笑",
"fa-solid fa-grin-hearts": "爱心眼笑",
"fa-solid fa-grin-squint": "眯眼笑",
"fa-solid fa-grin-squint-tears": "笑哭",
"fa-solid fa-grin-stars": "星星眼笑",
"fa-solid fa-grin-tears": "笑出眼泪",
"fa-solid fa-grin-tongue": "吐舌笑",
"fa-solid fa-grin-tongue-squint": "调皮吐舌",
"fa-solid fa-grin-tongue-wink": "眨眼吐舌",
"fa-solid fa-grin-wink": "眨眼笑",
"fa-solid fa-kiss": "亲吻",
"fa-solid fa-kiss-beam": "开心亲吻",
"fa-solid fa-kiss-wink-heart": "眨眼爱心吻",
"fa-solid fa-laugh": "大笑",
"fa-solid fa-laugh-beam": "开心大笑",
"fa-solid fa-laugh-squint": "眯眼大笑",
"fa-solid fa-laugh-wink": "眨眼大笑",
"fa-solid fa-meh": "中性",
"fa-solid fa-meh-blank": "面无表情",
"fa-solid fa-meh-rolling-eyes": "翻白眼",
"fa-solid fa-sad-cry": "伤心哭泣",
"fa-solid fa-sad-tear": "流泪",
"fa-solid fa-smile": "微笑",
"fa-solid fa-smile-beam": "开心微笑",
"fa-solid fa-smile-wink": "眨眼微笑",
"fa-solid fa-surprise": "惊讶",
"fa-solid fa-tired": "疲惫",
"fa-solid fa-user-astronaut": "宇航员",
"fa-solid fa-user-ninja": "忍者",
"fa-solid fa-user-secret": "特工",
"fa-solid fa-user-tie": "西装用户",
"fa-solid fa-ghost": "幽灵",
"fa-solid fa-poo": "便便",
"fa-solid fa-robot": "机器人",
"fa-solid fa-cat": "猫",
"fa-solid fa-dog": "狗",
"fa-solid fa-dove": "和平鸽",
"fa-solid fa-dragon": "龙",
"fa-solid fa-feather": "羽毛",
"fa-solid fa-feather-alt": "羽毛笔",
"fa-solid fa-frog": "青蛙",
"fa-solid fa-hippo": "河马",
"fa-solid fa-horse": "马",
"fa-solid fa-horse-head": "马头",
"fa-solid fa-kiwi-bird": "几维鸟",
"fa-solid fa-otter": "水獭",
"fa-solid fa-paw": "爪子",
"fa-solid fa-spider": "蜘蛛",
"fa-solid fa-crow": "乌鸦",
"fa-solid fa-ankh": "安卡",
"fa-solid fa-bahai": "巴哈伊",
"fa-solid fa-bible": "圣经",
"fa-solid fa-book-dead": "死灵之书",
"fa-solid fa-book-open": "打开的书",
"fa-solid fa-book-reader": "阅读器",
"fa-solid fa-church": "教堂",
"fa-solid fa-cross": "十字架",
"fa-solid fa-dharmachakra": "法轮",
"fa-solid fa-dice-d20": "20面骰",
"fa-solid fa-dice-d6": "6面骰",
"fa-solid fa-dice-five": "骰子5",
"fa-solid fa-dice-four": "骰子4",
"fa-solid fa-dice-one": "骰子1",
"fa-solid fa-dice-six": "骰子6",
"fa-solid fa-dice-three": "骰子3",
"fa-solid fa-dice-two": "骰子2",
"fa-solid fa-dragon": "龙",
"fa-solid fa-eye-evil": "邪恶之眼",
"fa-solid fa-fist-raised": "拳头",
"fa-solid fa-flask-poison": "毒药瓶",
"fa-solid fa-flask-potion": "药水瓶",
"fa-solid fa-fort-awesome": "堡垒",
"fa-solid fa-gopuram": "印度塔",
"fa-solid fa-hamsa": "哈姆萨",
"fa-solid fa-hand-holding-magic": "魔法手",
"fa-solid fa-hat-wizard": "巫师帽",
"fa-solid fa-haykal": "巴哈伊符号",
"fa-solid fa-jedi": "绝地",
"fa-solid fa-journal-whills": "绝地圣典",
"fa-solid fa-kaaba": "天房",
"fa-solid fa-khanda": "坎达剑",
"fa-solid fa-magic": "魔法",
"fa-solid fa-mandolin": "曼陀林",
"fa-solid fa-mask": "面具",
"fa-solid fa-menorah": "烛台",
"fa-solid fa-mosque": "清真寺",
"fa-solid fa-om": "唵",
"fa-solid fa-pastafarianism": "飞天面条",
"fa-solid fa-peace": "和平",
"fa-solid fa-place-of-worship": "礼拜场所",
"fa-solid fa-pray": "祈祷",
"fa-solid fa-praying-hands": "祈祷之手",
"fa-solid fa-quran": "古兰经",
"fa-solid fa-scroll": "卷轴",
"fa-solid fa-star-and-crescent": "星月",
"fa-solid fa-star-of-david": "大卫之星",
"fa-solid fa-synagogue": "犹太教堂",
"fa-solid fa-torah": "托拉",
"fa-solid fa-vihara": "寺院",
"fa-solid fa-volume-mute": "静音",
"fa-solid fa-volume-down": "音量减小",
"fa-solid fa-volume-up": "音量增大",
"fa-solid fa-volume-off": "音量关闭",
"fa-solid fa-wifi": "WiFi",
"fa-solid fa-signal": "信号",
"fa-solid fa-battery-empty": "电池空",
"fa-solid fa-battery-quarter": "电池25%",
"fa-solid fa-battery-half": "电池50%",
"fa-solid fa-battery-three-quarters": "电池75%",
"fa-solid fa-battery-full": "电池满",
"fa-solid fa-plug": "插头",
"fa-solid fa-power-off": "关机",
"fa-solid fa-lightbulb": "灯泡",
"fa-solid fa-mobile": "手机",
"fa-solid fa-tablet": "平板",
"fa-solid fa-laptop": "笔记本",
"fa-solid fa-desktop": "台式机",
"fa-solid fa-sim-card": "SIM卡",
"fa-solid fa-memory": "内存",
"fa-solid fa-hdd": "硬盘",
"fa-solid fa-ethernet": "以太网",
"fa-solid fa-sd-card": "SD卡",
"fa-solid fa-usb": "USB",
"fa-solid fa-bluetooth": "蓝牙",
"fa-solid fa-bluetooth-b": "蓝牙B",
"fa-solid fa-nfc": "NFC",
"fa-solid fa-network-wired": "有线网络",
"fa-solid fa-satellite-dish": "卫星天线",
"fa-solid fa-tv": "电视",
"fa-solid fa-projector": "投影仪",
"fa-solid fa-robot": "机器人",
"fa-solid fa-microchip": "芯片",
"fa-solid fa-server": "服务器",
"fa-solid fa-database": "数据库",
"fa-solid fa-hdd": "硬盘",
"fa-solid fa-save": "保存",
"fa-solid fa-upload": "上传",
"fa-solid fa-download": "下载",
"fa-solid fa-cloud-upload-alt": "云上传",
"fa-solid fa-cloud-download-alt": "云下载",
"fa-solid fa-cloud": "云",
"fa-solid fa-cloud-meatball": "云肉丸",
"fa-solid fa-cloud-moon": "云月亮",
"fa-solid fa-cloud-moon-rain": "云月雨",
"fa-solid fa-cloud-rain": "云雨",
"fa-solid fa-cloud-showers-heavy": "暴雨",
"fa-solid fa-cloud-sun": "云太阳",
"fa-solid fa-cloud-sun-rain": "云阳雨",
"fa-solid fa-meteor": "流星",
"fa-solid fa-moon": "月亮",
"fa-solid fa-poo-storm": "便便风暴",
"fa-solid fa-rainbow": "彩虹",
"fa-solid fa-smog": "雾霾",
"fa-solid fa-snowflake": "雪花",
"fa-solid fa-sun": "太阳",
"fa-solid fa-temperature-high": "高温",
"fa-solid fa-temperature-low": "低温",
"fa-solid fa-tint": "水滴",
"fa-solid fa-tint-slash": "无水",
"fa-solid fa-umbrella": "雨伞",
"fa-solid fa-water": "水",
"fa-solid fa-wind": "风",
"fa-solid fa-adjust": "调整",
"fa-solid fa-bolt": "闪电",
"fa-solid fa-camera": "相机",
"fa-solid fa-camera-retro": "复古相机",
"fa-solid fa-chalkboard": "黑板",
"fa-solid fa-clipboard": "剪贴板",
"fa-solid fa-comment": "评论",
"fa-solid fa-comment-alt": "评论框",
"fa-solid fa-comments": "多条评论",
"fa-solid fa-copy": "复制",
"fa-solid fa-cut": "剪切",
"fa-solid fa-edit": "编辑",
"fa-solid fa-eraser": "橡皮擦",
"fa-solid fa-file": "文件",
"fa-solid fa-file-alt": "文本文件",
"fa-solid fa-file-archive": "压缩文件",
"fa-solid fa-file-audio": "音频文件",
"fa-solid fa-file-code": "代码文件",
"fa-solid fa-file-excel": "Excel文件",
"fa-solid fa-file-image": "图片文件",
"fa-solid fa-file-pdf": "PDF文件",
"fa-solid fa-file-powerpoint": "PPT文件",
"fa-solid fa-file-video": "视频文件",
"fa-solid fa-file-word": "Word文件",
"fa-solid fa-folder": "文件夹",
"fa-solid fa-folder-open": "打开文件夹",
"fa-solid fa-font": "字体",
"fa-solid fa-glasses": "眼镜",
"fa-solid fa-highlighter": "荧光笔",
"fa-solid fa-i-cursor": "I型光标",
"fa-solid fa-keyboard": "键盘",
"fa-solid fa-marker": "马克笔",
"fa-solid fa-paperclip": "回形针",
"fa-solid fa-paste": "粘贴",
"fa-solid fa-pen": "钢笔",
"fa-solid fa-pen-alt": "钢笔替代",
"fa-solid fa-pen-fancy": "花式笔",
"fa-solid fa-pen-nib": "笔尖",
"fa-solid fa-pencil-alt": "铅笔",
"fa-solid fa-print": "打印",
"fa-solid fa-quote-left": "左引号",
"fa-solid fa-quote-right": "右引号",
"fa-solid fa-stamp": "印章",
"fa-solid fa-sticky-note": "便签",
"fa-solid fa-table": "表格",
"fa-solid fa-tasks": "任务",
"fa-solid fa-thumbtack": "图钉",
"fa-solid fa-trash": "垃圾桶",
"fa-solid fa-trash-alt": "垃圾桶(替代)",
"fa-solid fa-underline": "下划线",
"fa-solid fa-unlink": "取消链接",
"fa-solid fa-window-maximize": "窗口最大化",
"fa-solid fa-window-minimize": "窗口最小化",
"fa-solid fa-window-restore": "窗口恢复",
"fa-solid fa-align-center": "居中对齐",
"fa-solid fa-align-justify": "两端对齐",
"fa-solid fa-align-left": "左对齐",
"fa-solid fa-align-right": "右对齐",
"fa-solid fa-bold": "粗体",
"fa-solid fa-border-all": "所有边框",
"fa-solid fa-border-none": "无边框",
"fa-solid fa-border-style": "边框样式",
"fa-solid fa-clipboard-check": "剪贴板检查",
"fa-solid fa-clipboard-list": "剪贴板列表",
"fa-solid fa-columns": "列",
"fa-solid fa-copyright": "版权",
"fa-solid fa-ellipsis-h": "水平省略号",
"fa-solid fa-ellipsis-v": "垂直省略号",
"fa-solid fa-file-signature": "文件签名",
"fa-solid fa-font-awesome-logo-full": "Font Awesome标志",
"fa-solid fa-heading": "标题",
"fa-solid fa-horizontal-rule": "水平线",
"fa-solid fa-indent": "缩进",
"fa-solid fa-italic": "斜体",
"fa-solid fa-link": "链接",
"fa-solid fa-list": "列表",
"fa-solid fa-list-alt": "列表(替代)",
"fa-solid fa-list-ol": "有序列表",
"fa-solid fa-list-ul": "无序列表",
"fa-solid fa-outdent": "减少缩进",
"fa-solid fa-paragraph": "段落",
"fa-solid fa-poll": "投票",
"fa-solid fa-poll-h": "水平投票",
"fa-solid fa-redo": "重做",
"fa-solid fa-redo-alt": "重做(替代)",
"fa-solid fa-reply": "回复",
"fa-solid fa-reply-all": "回复全部",
"fa-solid fa-share-square": "分享方块",
"fa-solid fa-spell-check": "拼写检查",
"fa-solid fa-strikethrough": "删除线",
"fa-solid fa-subscript": "下标",
"fa-solid fa-superscript": "上标",
"fa-solid fa-sync": "同步",
"fa-solid fa-sync-alt": "同步(替代)",
"fa-solid fa-table": "表格",
"fa-solid fa-text-height": "文本高度",
"fa-solid fa-text-width": "文本宽度",
"fa-solid fa-th": "表格标题",
"fa-solid fa-th-large": "大表格",
"fa-solid fa-th-list": "表格列表",
"fa-solid fa-trash-restore": "恢复删除",
"fa-solid fa-trash-restore-alt": "恢复删除(替代)",
"fa-solid fa-undo": "撤销",
"fa-solid fa-undo-alt": "撤销(替代)",
"fa-solid fa-unlink": "取消链接",
"fa-solid fa-wrench": "扳手",
"fa-brands fa-500px": "500px",
"fa-brands fa-accessible-icon": "无障碍图标",
"fa-brands fa-accusoft": "Accusoft",
"fa-brands fa-acquisitions-incorporated": "Acquisitions Inc",
"fa-brands fa-adn": "ADN",
"fa-brands fa-adobe": "Adobe",
"fa-brands fa-adversal": "Adversal",
"fa-brands fa-affiliatetheme": "AffiliateTheme",
"fa-brands fa-airbnb": "Airbnb",
"fa-brands fa-algolia": "Algolia",
"fa-brands fa-alipay": "支付宝",
"fa-brands fa-amazon": "亚马逊",
"fa-brands fa-amazon-pay": "Amazon Pay",
"fa-brands fa-amilia": "Amilia",
"fa-brands fa-android": "安卓",
"fa-brands fa-angellist": "AngelList",
"fa-brands fa-angrycreative": "Angry Creative",
"fa-brands fa-angular": "Angular",
"fa-brands fa-app-store": "App Store",
"fa-brands fa-app-store-ios": "App Store iOS",
"fa-brands fa-apper": "Apper",
"fa-brands fa-apple": "苹果",
"fa-brands fa-apple-pay": "Apple Pay",
"fa-brands fa-artstation": "ArtStation",
"fa-brands fa-asymmetrik": "Asymmetrik",
"fa-brands fa-atlassian": "Atlassian",
"fa-brands fa-audible": "Audible",
"fa-brands fa-autoprefixer": "Autoprefixer",
"fa-brands fa-avianex": "Avianex",
"fa-brands fa-aviato": "Aviato",
"fa-brands fa-aws": "AWS",
"fa-brands fa-bandcamp": "Bandcamp",
"fa-brands fa-battle-net": "暴雪战网",
"fa-brands fa-behance": "Behance",
"fa-brands fa-behance-square": "Behance方块",
"fa-brands fa-bimobject": "BIMobject",
"fa-brands fa-bitbucket": "Bitbucket",
"fa-brands fa-bitcoin": "比特币",
"fa-brands fa-bity": "Bity",
"fa-brands fa-black-tie": "Black Tie",
"fa-brands fa-blackberry": "黑莓",
"fa-brands fa-blogger": "Blogger",
"fa-brands fa-blogger-b": "Blogger B",
"fa-brands fa-bluetooth": "蓝牙",
"fa-brands fa-bluetooth-b": "蓝牙B",
"fa-brands fa-bootstrap": "Bootstrap",
"fa-brands fa-btc": "BTC",
"fa-brands fa-buffer": "Buffer",
"fa-brands fa-buromobelexperte": "Büromöbel Experte",
"fa-brands fa-buy-n-large": "Buy n Large",
"fa-brands fa-buysellads": "BuySellAds",
"fa-brands fa-canadian-maple-leaf": "加拿大枫叶",
"fa-brands fa-cc-amazon-pay": "CC Amazon Pay",
"fa-brands fa-cc-amex": "美国运通",
"fa-brands fa-cc-apple-pay": "CC Apple Pay",
"fa-brands fa-cc-diners-club": "大来卡",
"fa-brands fa-cc-discover": "Discover卡",
"fa-brands fa-cc-jcb": "JCB卡",
"fa-brands fa-cc-mastercard": "万事达卡",
"fa-brands fa-cc-paypal": "PayPal卡",
"fa-brands fa-cc-stripe": "Stripe卡",
"fa-brands fa-cc-visa": "Visa卡",
"fa-brands fa-centercode": "Centercode",
"fa-brands fa-centos": "CentOS",
"fa-brands fa-chrome": "Chrome",
"fa-brands fa-chromecast": "Chromecast",
"fa-brands fa-cloudflare": "Cloudflare",
"fa-brands fa-cloudscale": "Cloudscale",
"fa-brands fa-cloudsmith": "Cloudsmith",
"fa-brands fa-cloudversify": "Cloudversify",
"fa-brands fa-codepen": "CodePen",
"fa-brands fa-codiepie": "CodiePie",
"fa-brands fa-confluence": "Confluence",
"fa-brands fa-connectdevelop": "Connect Develop",
"fa-brands fa-contao": "Contao",
"fa-brands fa-cotton-bureau": "Cotton Bureau",
"fa-brands fa-cpanel": "cPanel",
"fa-brands fa-creative-commons": "Creative Commons",
"fa-brands fa-creative-commons-by": "CC BY",
"fa-brands fa-creative-commons-nc": "CC NC",
"fa-brands fa-creative-commons-nc-eu": "CC NC EU",
"fa-brands fa-creative-commons-nc-jp": "CC NC JP",
"fa-brands fa-creative-commons-nd": "CC ND",
"fa-brands fa-creative-commons-pd": "CC PD",
"fa-brands fa-creative-commons-pd-alt": "CC PD Alt",
"fa-brands fa-creative-commons-remix": "CC Remix",
"fa-brands fa-creative-commons-sa": "CC SA",
"fa-brands fa-creative-commons-sampling": "CC Sampling",
"fa-brands fa-creative-commons-sampling-plus": "CC Sampling Plus",
"fa-brands fa-creative-commons-share": "CC Share",
"fa-brands fa-creative-commons-zero": "CC Zero",
"fa-brands fa-critical-role": "Critical Role",
"fa-brands fa-css3": "CSS3",
"fa-brands fa-css3-alt": "CSS3 Alt",
"fa-brands fa-cuttlefish": "Cuttlefish",
"fa-brands fa-d-and-d": "D&D",
"fa-brands fa-d-and-d-beyond": "D&D Beyond",
"fa-brands fa-dailymotion": "Dailymotion",
"fa-brands fa-dashcube": "Dashcube",
"fa-brands fa-deezer": "Deezer",
"fa-brands fa-delicious": "Delicious",
"fa-brands fa-deploydog": "Deploydog",
"fa-brands fa-deskpro": "Deskpro",
"fa-brands fa-dev": "DEV",
"fa-brands fa-deviantart": "DeviantArt",
"fa-brands fa-dhl": "DHL",
"fa-brands fa-diaspora": "Diaspora",
"fa-brands fa-digg": "Digg",
"fa-brands fa-digital-ocean": "DigitalOcean",
"fa-brands fa-discord": "Discord",
"fa-brands fa-discourse": "Discourse",
"fa-brands fa-dochub": "DocHub",
"fa-brands fa-docker": "Docker",
"fa-brands fa-draft2digital": "Draft2Digital",
"fa-brands fa-dribbble": "Dribbble",
"fa-brands fa-dribbble-square": "Dribbble方块",
"fa-brands fa-dropbox": "Dropbox",
"fa-brands fa-drupal": "Drupal",
"fa-brands fa-dyalog": "Dyalog",
"fa-brands fa-earlybirds": "Earlybirds",
"fa-brands fa-ebay": "eBay",
"fa-brands fa-edge": "Edge",
"fa-brands fa-edge-legacy": "Edge Legacy",
"fa-brands fa-elementor": "Elementor",
"fa-brands fa-ello": "Ello",
"fa-brands fa-ember": "Ember",
"fa-brands fa-empire": "Empire",
"fa-brands fa-envira": "Envira",
"fa-brands fa-erlang": "Erlang",
"fa-brands fa-ethereum": "以太坊",
"fa-brands fa-etsy": "Etsy",
"fa-brands fa-evernote": "Evernote",
"fa-brands fa-expeditedssl": "ExpeditedSSL",
"fa-brands fa-facebook": "Facebook",
"fa-brands fa-facebook-f": "Facebook F",
"fa-brands fa-facebook-messenger": "Facebook Messenger",
"fa-brands fa-facebook-square": "Facebook方块",
"fa-brands fa-fantasy-flight-games": "Fantasy Flight Games",
"fa-brands fa-fedex": "FedEx",
"fa-brands fa-fedora": "Fedora",
"fa-brands fa-figma": "Figma",
"fa-brands fa-firefox": "Firefox",
"fa-brands fa-firefox-browser": "Firefox浏览器",
"fa-brands fa-first-order": "First Order",
"fa-brands fa-first-order-alt": "First Order Alt",
"fa-brands fa-firstdraft": "Firstdraft",
"fa-brands fa-flickr": "Flickr",
"fa-brands fa-flipboard": "Flipboard",
"fa-brands fa-fly": "Fly",
"fa-brands fa-font-awesome": "Font Awesome",
"fa-brands fa-font-awesome-alt": "Font Awesome Alt",
"fa-brands fa-font-awesome-flag": "Font Awesome标志",
"fa-brands fa-fonticons": "Fonticons",
"fa-brands fa-fonticons-fi": "Fonticons Fi",
"fa-brands fa-fort-awesome": "Fort Awesome",
"fa-brands fa-fort-awesome-alt": "Fort Awesome Alt",
"fa-brands fa-forumbee": "Forumbee",
"fa-brands fa-foursquare": "Foursquare",
"fa-brands fa-free-code-camp": "Free Code Camp",
"fa-brands fa-freebsd": "FreeBSD",
"fa-brands fa-fulcrum": "Fulcrum",
"fa-brands fa-galactic-republic": "银河共和国",
"fa-brands fa-galactic-senate": "银河参议院",
"fa-brands fa-get-pocket": "Pocket",
"fa-brands fa-gg": "GG",
"fa-brands fa-gg-circle": "GG圆圈",
"fa-brands fa-git": "Git",
"fa-brands fa-git-alt": "Git Alt",
"fa-brands fa-git-square": "Git方块",
"fa-brands fa-github": "GitHub",
"fa-brands fa-github-alt": "GitHub Alt",
"fa-brands fa-github-square": "GitHub方块",
"fa-brands fa-gitkraken": "GitKraken",
"fa-brands fa-gitlab": "GitLab",
"fa-brands fa-gitter": "Gitter",
"fa-brands fa-glide": "Glide",
"fa-brands fa-glide-g": "Glide G",
"fa-brands fa-gofore": "Gofore",
"fa-brands fa-goodreads": "Goodreads",
"fa-brands fa-goodreads-g": "Goodreads G",
"fa-brands fa-google": "Google",
"fa-brands fa-google-drive": "Google Drive",
"fa-brands fa-google-pay": "Google Pay",
"fa-brands fa-google-play": "Google Play",
"fa-brands fa-google-plus": "Google+",
"fa-brands fa-google-plus-g": "Google+ G",
"fa-brands fa-google-plus-square": "Google+方块",
"fa-brands fa-google-wallet": "Google Wallet",
"fa-brands fa-gratipay": "Gratipay",
"fa-brands fa-grav": "Grav",
"fa-brands fa-gripfire": "Gripfire",
"fa-brands fa-grunt": "Grunt",
"fa-brands fa-guilded": "Guilded",
"fa-brands fa-gulp": "Gulp",
"fa-brands fa-hacker-news": "Hacker News",
"fa-brands fa-hacker-news-square": "Hacker News方块",
"fa-brands fa-hackerrank": "HackerRank",
"fa-brands fa-hips": "HIPS",
"fa-brands fa-hire-a-helper": "HireAHelper",
"fa-brands fa-hive": "Hive",
"fa-brands fa-hooli": "Hooli",
"fa-brands fa-hornbill": "Hornbill",
"fa-brands fa-hotjar": "Hotjar",
"fa-brands fa-houzz": "Houzz",
"fa-brands fa-html5": "HTML5",
"fa-brands fa-hubspot": "HubSpot",
"fa-brands fa-ideal": "iDEAL",
"fa-brands fa-imdb": "IMDb",
"fa-brands fa-instagram": "Instagram",
"fa-brands fa-instagram-square": "Instagram方块",
"fa-brands fa-intercom": "Intercom",
"fa-brands fa-internet-explorer": "IE",
"fa-brands fa-invision": "InVision",
"fa-brands fa-ioxhost": "ioxhost",
"fa-brands fa-itch-io": "itch.io",
"fa-brands fa-itunes": "iTunes",
"fa-brands fa-itunes-note": "iTunes Note",
"fa-brands fa-java": "Java",
"fa-brands fa-jedi-order": "绝地武士团",
"fa-brands fa-jenkins": "Jenkins",
"fa-brands fa-jira": "Jira",
"fa-brands fa-joget": "Joget",
"fa-brands fa-joomla": "Joomla",
"fa-brands fa-js": "JS",
"fa-brands fa-js-square": "JS方块",
"fa-brands fa-jsfiddle": "JSFiddle",
"fa-brands fa-kaggle": "Kaggle",
"fa-brands fa-keybase": "Keybase",
"fa-brands fa-keycdn": "KeyCDN",
"fa-brands fa-kickstarter": "Kickstarter",
"fa-brands fa-kickstarter-k": "Kickstarter K",
"fa-brands fa-korvue": "Korvue",
"fa-brands fa-laravel": "Laravel",
"fa-brands fa-lastfm": "Last.fm",
"fa-brands fa-lastfm-square": "Last.fm方块",
"fa-brands fa-leanpub": "Leanpub",
"fa-brands fa-less": "Less",
"fa-brands fa-line": "Line",
"fa-brands fa-linkedin": "LinkedIn",
"fa-brands fa-linkedin-in": "LinkedIn In",
"fa-brands fa-linode": "Linode",
"fa-brands fa-linux": "Linux",
"fa-brands fa-lyft": "Lyft",
"fa-brands fa-magento": "Magento",
"fa-brands fa-mailchimp": "Mailchimp",
"fa-brands fa-mandalorian": "曼达洛人",
"fa-brands fa-markdown": "Markdown",
"fa-brands fa-mastodon": "Mastodon",
"fa-brands fa-maxcdn": "MaxCDN",
"fa-brands fa-mdb": "MDB",
"fa-brands fa-medapps": "MedApps",
"fa-brands fa-medium": "Medium",
"fa-brands fa-medium-m": "Medium M",
"fa-brands fa-medrt": "Medrt",
"fa-brands fa-meetup": "Meetup",
"fa-brands fa-megaport": "Megaport",
"fa-brands fa-mendeley": "Mendeley",
"fa-brands fa-microblog": "Micro.blog",
"fa-brands fa-microsoft": "微软",
"fa-brands fa-mix": "Mix",
"fa-brands fa-mixcloud": "Mixcloud",
"fa-brands fa-mixer": "Mixer",
"fa-brands fa-mizuni": "Mizuni",
"fa-brands fa-modx": "MODX",
"fa-brands fa-monero": "门罗币",
"fa-brands fa-napster": "Napster",
"fa-brands fa-neos": "Neos",
"fa-brands fa-nimblr": "Nimblr",
"fa-brands fa-node": "Node.js",
"fa-brands fa-node-js": "Node.js",
"fa-brands fa-npm": "npm",
"fa-brands fa-ns8": "NS8",
"fa-brands fa-nutritionix": "Nutritionix",
"fa-brands fa-octopus-deploy": "Octopus Deploy",
"fa-brands fa-odnoklassniki": "Odnoklassniki",
"fa-brands fa-odnoklassniki-square": "Odnoklassniki方块",
"fa-brands fa-old-republic": "旧共和国",
"fa-brands fa-opencart": "OpenCart",
"fa-brands fa-openid": "OpenID",
"fa-brands fa-opera": "Opera",
"fa-brands fa-optin-monster": "Optin Monster",
"fa-brands fa-orcid": "ORCID",
"fa-brands fa-osi": "OSI",
"fa-brands fa-page4": "Page4",
"fa-brands fa-pagelines": "Pagelines",
"fa-brands fa-palfed": "Palfed",
"fa-brands fa-patreon": "Patreon",
"fa-brands fa-paypal": "PayPal",
"fa-brands fa-penny-arcade": "Penny Arcade",
"fa-brands fa-periscope": "Periscope",
"fa-brands fa-phabricator": "Phabricator",
"fa-brands fa-phoenix-framework": "Phoenix Framework",
"fa-brands fa-phoenix-squadron": "凤凰中队",
"fa-brands fa-php": "PHP",
"fa-brands fa-pied-piper": "Pied Piper",
"fa-brands fa-pied-piper-alt": "Pied Piper Alt",
"fa-brands fa-pied-piper-hat": "Pied Piper Hat",
"fa-brands fa-pied-piper-pp": "Pied Piper PP",
"fa-brands fa-pied-piper-square": "Pied Piper方块",
"fa-brands fa-pinterest": "Pinterest",
"fa-brands fa-pinterest-p": "Pinterest P",
"fa-brands fa-pinterest-square": "Pinterest方块",
"fa-brands fa-playstation": "PlayStation",
"fa-brands fa-product-hunt": "Product Hunt",
"fa-brands fa-pushed": "Pushed",
"fa-brands fa-python": "Python",
"fa-brands fa-qq": "QQ",
"fa-brands fa-quinscape": "QuinScape",
"fa-brands fa-quora": "Quora",
"fa-brands fa-r-project": "R Project",
"fa-brands fa-raspberry-pi": "树莓派",
"fa-brands fa-ravelry": "Ravelry",
"fa-brands fa-react": "React",
"fa-brands fa-reacteurope": "ReactEurope",
"fa-brands fa-readme": "Readme",
"fa-brands fa-rebel": "Rebel",
"fa-brands fa-red-river": "Red River",
"fa-brands fa-reddit": "Reddit",
"fa-brands fa-reddit-alien": "Reddit Alien",
"fa-brands fa-reddit-square": "Reddit方块",
"fa-brands fa-redhat": "Red Hat",
"fa-brands fa-renren": "人人网",
"fa-brands fa-replyd": "Replyd",
"fa-brands fa-researchgate": "ResearchGate",
"fa-brands fa-resolving": "Resolving",
"fa-brands fa-rev": "Rev",
"fa-brands fa-rocketchat": "Rocket.Chat",
"fa-brands fa-rockrms": "Rockrms",
"fa-brands fa-rust": "Rust",
"fa-brands fa-safari": "Safari",
"fa-brands fa-salesforce": "Salesforce",
"fa-brands fa-sass": "Sass",
"fa-brands fa-schlix": "Schlix",
"fa-brands fa-scribd": "Scribd",
"fa-brands fa-searchengin": "搜索引擎",
"fa-brands fa-sellcast": "Sellcast",
"fa-brands fa-sellsy": "Sellsy",
"fa-brands fa-servicestack": "ServiceStack",
"fa-brands fa-shirtsinbulk": "Shirts In Bulk",
"fa-brands fa-shopify": "Shopify",
"fa-brands fa-shopware": "Shopware",
"fa-brands fa-simplybuilt": "SimplyBuilt",
"fa-brands fa-sistrix": "Sistrix",
"fa-brands fa-sith": "西斯",
"fa-brands fa-sketch": "Sketch",
"fa-brands fa-skyatlas": "Skyatlas",
"fa-brands fa-skype": "Skype",
"fa-brands fa-slack": "Slack",
"fa-brands fa-slack-hash": "Slack Hash",
"fa-brands fa-slideshare": "SlideShare",
"fa-brands fa-snapchat": "Snapchat",
"fa-brands fa-snapchat-ghost": "Snapchat Ghost",
"fa-brands fa-snapchat-square": "Snapchat方块",
"fa-brands fa-soundcloud": "SoundCloud",
"fa-brands fa-sourcetree": "SourceTree",
"fa-brands fa-speakap": "Speakap",
"fa-brands fa-speaker-deck": "Speaker Deck",
"fa-brands fa-spotify": "Spotify",
"fa-brands fa-squarespace": "Squarespace",
"fa-brands fa-stack-exchange": "Stack Exchange",
"fa-brands fa-stack-overflow": "Stack Overflow",
"fa-brands fa-stackpath": "Stackpath",
"fa-brands fa-staylinked": "StayLinked",
"fa-brands fa-steam": "Steam",
"fa-brands fa-steam-square": "Steam方块",
"fa-brands fa-steam-symbol": "Steam符号",
"fa-brands fa-sticker-mule": "Sticker Mule",
"fa-brands fa-strava": "Strava",
"fa-brands fa-stripe": "Stripe",
"fa-brands fa-stripe-s": "Stripe S",
"fa-brands fa-studiovinari": "StudioVinari",
"fa-brands fa-stumbleupon": "StumbleUpon",
"fa-brands fa-stumbleupon-circle": "StumbleUpon圆圈",
"fa-brands fa-superpowers": "Superpowers",
"fa-brands fa-supple": "Supple",
"fa-brands fa-suse": "SUSE",
"fa-brands fa-swift": "Swift",
"fa-brands fa-symfony": "Symfony",
"fa-brands fa-teamspeak": "TeamSpeak",
"fa-brands fa-telegram": "Telegram",
"fa-brands fa-telegram-plane": "Telegram Plane",
"fa-brands fa-tencent-weibo": "腾讯微博",
"fa-brands fa-the-red-yeti": "The Red Yeti",
"fa-brands fa-themeco": "Themeco",
"fa-brands fa-themeisle": "ThemeIsle",
"fa-brands fa-think-peaks": "Think Peaks",
"fa-brands fa-tiktok": "TikTok",
"fa-brands fa-trade-federation": "贸易联盟",
"fa-brands fa-trello": "Trello",
"fa-brands fa-tripadvisor": "TripAdvisor",
"fa-brands fa-tumblr": "Tumblr",
"fa-brands fa-tumblr-square": "Tumblr方块",
"fa-brands fa-twitch": "Twitch",
"fa-brands fa-twitter": "Twitter",
"fa-brands fa-twitter-square": "Twitter方块",
"fa-brands fa-typo3": "TYPO3",
"fa-brands fa-uber": "Uber",
"fa-brands fa-ubuntu": "Ubuntu",
"fa-brands fa-uikit": "UIkit",
"fa-brands fa-umbraco": "Umbraco",
"fa-brands fa-uncharted": "Uncharted",
"fa-brands fa-uniregistry": "Uniregistry",
"fa-brands fa-unity": "Unity",
"fa-brands fa-unsplash": "Unsplash",
"fa-brands fa-untappd": "Untappd",
"fa-brands fa-ups": "UPS",
"fa-brands fa-usb": "USB",
"fa-brands fa-usps": "USPS",
"fa-brands fa-ussunnah": "US Sunnah",
"fa-brands fa-vaadin": "Vaadin",
"fa-brands fa-viacoin": "Viacoin",
"fa-brands fa-viadeo": "Viadeo",
"fa-brands fa-viadeo-square": "Viadeo方块",
"fa-brands fa-viber": "Viber",
"fa-brands fa-vimeo": "Vimeo",
"fa-brands fa-vimeo-square": "Vimeo方块",
"fa-brands fa-vimeo-v": "Vimeo V",
"fa-brands fa-vine": "Vine",
"fa-brands fa-vk": "VK",
"fa-brands fa-vnv": "VNV",
"fa-brands fa-vuejs": "Vue.js",
"fa-brands fa-watchman-monitoring": "Watchman Monitoring",
"fa-brands fa-waze": "Waze",
"fa-brands fa-weebly": "Weebly",
"fa-brands fa-weibo": "微博",
"fa-brands fa-weixin": "微信",
"fa-brands fa-whatsapp": "WhatsApp",
"fa-brands fa-whatsapp-square": "WhatsApp方块",
"fa-brands fa-whmcs": "WHMCS",
"fa-brands fa-wikipedia-w": "Wikipedia W",
"fa-brands fa-windows": "Windows",
"fa-brands fa-wix": "Wix",
"fa-brands fa-wizards-of-the-coast": "海岸巫师",
"fa-brands fa-wodu": "WODU",
"fa-brands fa-wolf-pack-battalion": "Wolf Pack Battalion",
"fa-brands fa-wordpress": "WordPress",
"fa-brands fa-wordpress-simple": "WordPress Simple",
"fa-brands fa-wpbeginner": "WPBeginner",
"fa-brands fa-wpexplorer": "WPExplorer",
"fa-brands fa-wpforms": "WPForms",
"fa-brands fa-wpressr": "WPpressr",
"fa-brands fa-xbox": "Xbox",
"fa-brands fa-xing": "Xing",
"fa-brands fa-xing-square": "Xing方块",
"fa-brands fa-y-combinator": "Y Combinator",
"fa-brands fa-yahoo": "Yahoo",
"fa-brands fa-yammer": "Yammer",
"fa-brands fa-yandex": "Yandex",
"fa-brands fa-yandex-international": "Yandex International",
"fa-brands fa-yarn": "Yarn",
"fa-brands fa-yelp": "Yelp",
"fa-brands fa-yoast": "Yoast",
"fa-brands fa-youtube": "YouTube",
"fa-brands fa-youtube-square": "YouTube方块",
"fa-brands fa-zhihu": "知乎"
}

14
data/settings.json Normal file
View File

@ -0,0 +1,14 @@
{
"card_style": "compact",
"search_history": [],
"theme": "light",
"bg_image": "/upload/background/5dd4f5d3cd7b48eca9967fa063ea5cd9.png",
"dark_bg_image": "/static/background_dark.jpg",
"site_title": "应用导航",
"show_logo": true,
"logo_type": "image",
"logo_icon": "fa-solid fa-th-list",
"logo_image": "/upload/logo/f40e2eb965b24e358a5bba9523231f8f.png",
"dark_bg_rotate": false,
"admin_password_hash": "scrypt:32768:8:1$mPFCfRRzOrcjE6z3$e72ef50a2d3f7292f64bcfc5e21f32c95ea8665414ea8d5f6b216735d68f151166c99fae21132c7949bd92ea32041f969cd4a471adb110a99328089541f7dccb"
}

6
data/system_config.json Normal file
View File

@ -0,0 +1,6 @@
{
"site_title": "导航系统",
"default_theme": "dark",
"allow_registration": true,
"enable_captcha": true
}

View File

@ -0,0 +1,5 @@
{
"site_title": "前往导航",
"logo": "/static/logo.png",
"favicon": "/static/favicon.ico"
}

9
data/users.json Normal file
View File

@ -0,0 +1,9 @@
{
"admin": {
"password": "112233",
"theme": "light",
"card_style": "compact",
"bg_image": "/upload/background/admin_bg.png",
"dark_bg_image": "/upload/background/admin_dark_bg.png"
}
}

BIN
static/background_dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

BIN
static/background_light.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

6
static/css/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
static/css/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

7
static/js/bootstrap.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

216
templates/add_app.html Normal file
View File

@ -0,0 +1,216 @@
{% extends "base.html" %}
{% block title %}添加应用{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4>添加应用</h4>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label class="form-label">标题</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">URL</label>
<input type="url" name="url" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">图标</label>
<input type="hidden" name="icon" id="selectedIcon" value="" 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>
</div>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#iconModal">
<i class="fas fa-icons me-2"></i>选择图标
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">主分类</label>
<select name="main_category" id="main_category" class="form-select" required>
<option value="">请选择</option>
{% for main_id, cat in categories.items() %}
<option value="{{ main_id }}">{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">子分类</label>
<select name="sub_category" id="sub_category" class="form-select" required>
<option value="">请选择</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea name="description" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" name="private" class="form-check-input" id="privateCheck">
<label class="form-check-label" for="privateCheck">私有应用(仅登录后可见)</label>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" class="btn btn-primary me-md-2">
<i class="fas fa-save me-2"></i>添加
</button>
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>返回
</a>
</div>
</form>
</div>
</div>
<!-- 图标选择模态框 -->
<div class="modal fade" id="iconModal" tabindex="-1" aria-labelledby="iconModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="iconModalLabel">选择图标</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="input-group mb-3">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" id="iconSearch" class="form-control" placeholder="搜索图标...">
</div>
<div class="row row-cols-3 row-cols-sm-4 row-cols-md-5 row-cols-lg-6 row-cols-xl-8 g-3" id="iconGrid">
{% for icon, name in icons.items() %}
<div class="col">
<div class="icon-item p-3 text-center rounded" data-icon="{{ icon }}" title="{{ name }}">
<i class="{{ icon }} fa-2x mb-2"></i>
<div class="small text-truncate">{{ name }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="modal-footer">
<div class="me-auto">
<span id="selectedIconName">未选择图标</span>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmIcon">确认选择</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
let selectedIcon = '';
let selectedIconName = '';
// 图标搜索功能
document.getElementById('iconSearch').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const iconItems = document.querySelectorAll('.icon-item');
iconItems.forEach(item => {
const iconName = item.getAttribute('title').toLowerCase();
const iconCode = item.dataset.icon.toLowerCase();
if (iconName.includes(searchTerm) || iconCode.includes(searchTerm)) {
item.closest('.col').style.display = 'block';
} else {
item.closest('.col').style.display = 'none';
}
});
});
// 图标点击选择
document.querySelectorAll('.icon-item').forEach(item => {
item.addEventListener('click', function() {
// 移除所有选中状态
document.querySelectorAll('.icon-item').forEach(i => {
i.classList.remove('selected');
});
// 添加选中状态
this.classList.add('selected');
selectedIcon = this.dataset.icon;
selectedIconName = this.getAttribute('title');
// 更新底部显示
document.getElementById('selectedIconName').textContent = selectedIconName;
});
});
// 确认选择
document.getElementById('confirmIcon').addEventListener('click', function() {
if (selectedIcon) {
document.getElementById('selectedIcon').value = selectedIcon;
document.getElementById('iconPreview').className = selectedIcon + ' fa-2x';
bootstrap.Modal.getInstance(document.getElementById('iconModal')).hide();
} else {
alert('请先选择一个图标');
}
});
// 主分类变化时加载子分类
document.getElementById('main_category').addEventListener('change', function() {
const mainId = this.value;
if (!mainId) return;
fetch('/get_subcategories/' + mainId)
.then(res => res.json())
.then(data => {
const subSelect = document.getElementById('sub_category');
subSelect.innerHTML = '<option value="">请选择</option>';
for (const [id, name] of Object.entries(data)) {
const option = document.createElement('option');
option.value = id;
option.textContent = name;
subSelect.appendChild(option);
}
});
});
});
</script>
<style>
.icon-preview {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border-radius: 5px;
}
.icon-item {
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.icon-item:hover {
background-color: #f8f9fa;
border-color: #dee2e6;
}
.icon-item.selected {
background-color: #e7f1ff;
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
#iconGrid {
max-height: 60vh;
overflow-y: auto;
}
#selectedIconName {
font-weight: bold;
color: #0d6efd;
}
</style>
{% endblock %}

View File

@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}附件管理{% endblock %}
{% block content %}
<div class="card">
<div class="card-header bg-primary text-white">
<h4><i class="fas fa-paperclip me-2"></i>附件管理</h4>
</div>
<div class="card-body">
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">上传新附件</h5>
<form method="POST" action="{{ url_for('upload_attachment') }}" enctype="multipart/form-data">
<div class="row">
<div class="col-md-4 mb-3">
<label for="type" class="form-label">附件类型</label>
<select name="type" id="type" class="form-select" required>
<option value="logo">Logo</option>
<option value="background">背景图片</option>
<option value="video">背景视频</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="file" class="form-label">选择文件</label>
<input type="file" class="form-control" id="file" name="file" accept="image/*,video/mp4" required>
</div>
<div class="col-md-2 mb-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>上传
</button>
</div>
</div>
</form>
</div>
<div class="mb-4">
<h5 class="border-bottom pb-2 mb-3">附件列表</h5>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>预览</th>
<th>文件名</th>
<th>类型</th>
<th>上传时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for attachment in attachments %}
<tr>
<td>
{% if attachment.type == 'logo' %}
<img src="{{ url_for('uploaded_logo', filename=attachment.filename) }}" style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
{% elif attachment.type == 'background' %}
<img src="{{ url_for('uploaded_background', filename=attachment.filename) }}" style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
{% else %}
<video width="80" height="45" muted style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
<source src="{{ url_for('uploaded_video', filename=attachment.filename) }}" type="video/mp4">
</video>
{% endif %}
</td>
<td>{{ attachment.filename }}</td>
<td>
{% if attachment.type == 'logo' %}
Logo
{% elif attachment.type == 'background' %}
背景图片
{% else %}
背景视频
{% endif %}
</td>
<td>{{ attachment.upload_time }}</td>
<td>
<form method="POST" action="{{ url_for('delete_attachment_route', filename=attachment.filename) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除这个附件吗?')">
<i class="fas fa-trash"></i> 删除
</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center">暂无附件</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="mt-4">
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回首页
</a>
</div>
</div>
</div>
{% endblock %}

128
templates/base.html Normal file
View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}前往导航{% endblock %}</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>
body {
padding-top: 20px;
padding-bottom: 40px;
}
.navbar {
margin-bottom: 20px;
}
.footer {
margin-top: 40px;
padding: 20px 0;
border-top: 1px solid #eee;
text-align: center;
color: #777;
}
/* 添加选中状态的样式 */
.nav-item .nav-link.active {
font-weight: bold;
color: #0d6efd !important;
border-bottom: 2px solid #0d6efd;
}
.badge .badge {
padding: 0.2em 0.4em;
font-size: 0.75em;
line-height: 1;
}
.bg-primary {
--bs-bg-opacity: 1;
background-color: rgb(106 113 124) !important;
}
.bg-light {
--bs-bg-opacity: 1;
background-color: rgb(220 224 227) !important;
}
</style>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded">
<div class="container-fluid">
{% if settings and settings.show_logo and settings.logo_type == 'image' and settings.logo_image %}
<a class="navbar-brand" href="{{ url_for('navigation') }}">
<img src="{{ settings.logo_image }}" height="30" class="d-inline-block align-top me-2" alt="Logo">
{{ settings.site_title if settings.site_title else '前往导航' }}
</a>
{% elif settings and settings.show_logo and settings.logo_type == 'icon' and settings.logo_icon %}
<a class="navbar-brand" href="{{ url_for('navigation') }}">
<i class="{{ settings.logo_icon }} me-2"></i>
{{ settings.site_title if settings.site_title else '前往导航' }}
</a>
{% else %}
<a class="navbar-brand" href="{{ url_for('navigation') }}">前往导航</a>
{% endif %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{% if 'username' in session %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'index' %}active{% endif %}" href="{{ url_for('index') }}">应用管理</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'manage_categories' %}active{% endif %}" href="{{ url_for('manage_categories') }}">分类管理</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'manage_attachments' %}active{% endif %}" href="{{ url_for('manage_attachments') }}">附件管理</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if 'username' in session %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> {{ session['username'] }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item {% if request.endpoint == 'system_settings' %}active{% endif %}" href="{{ url_for('system_settings') }}">系统设置</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">退出登录</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'login' %}active{% endif %}" href="{{ url_for('login') }}">登录</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
<footer class="footer">
<p>© 2025 导航管理系统</p>
</footer>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

112
templates/categories.html Normal file
View File

@ -0,0 +1,112 @@
{% extends "base.html" %}
{% block title %}分类管理{% endblock %}
{% block content %}
<div class="card">
<div class="card-header bg-primary text-white">
<h4><i class="fas fa-folder me-2"></i>分类管理</h4>
</div>
<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">
<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">
<i class="fas fa-plus"></i> 添加
</button>
</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;">
<option value="">选择主分类</option>
{% for main_id, cat in categories.items() %}
<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">
<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">
<i class="fas fa-plus"></i> 添加
</button>
</form>
</div>
<div>
<h5>已存在的分类</h5>
<div class="list-group">
{% for main_id, cat in categories.items() %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ cat.name }}</strong>ID: {{ main_id }})
<span class="badge" style="background-color: {{ cat.color }}; color: white;">{{ cat.color }}</span>
{% if cat.get('private', False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</div>
<div>
<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> 编辑
</a>
<a href="{{ url_for('delete_main_category', main_id=main_id) }}"
class="btn btn-sm btn-danger"
onclick="return confirm('确认删除整个主分类?')">
<i class="fas fa-trash"></i> 删除
</a>
</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>
{{ subData.name }}ID: {{ sub_id }})
<span class="badge" style="background-color: {{ subData.color }}; color: white;">{{ subData.color }}</span>
{% if cat.get('sub_private', {}).get(sub_id, False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</span>
<div>
<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> 编辑
</a>
<a href="{{ url_for('delete_sub_category', main_id=main_id, sub_id=sub_id) }}"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('确认删除该子分类?')">
<i class="fas fa-trash"></i> 删除
</a>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
<div class="mt-4">
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回首页
</a>
</div>
</div>
</div>
{% endblock %}

242
templates/edit_app.html Normal file
View File

@ -0,0 +1,242 @@
{% extends "base.html" %}
{% block title %}编辑应用{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4>编辑应用</h4>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label class="form-label">标题</label>
<input type="text" name="title" class="form-control" value="{{ app.title }}" required>
</div>
<div class="mb-3">
<label class="form-label">URL</label>
<input type="url" name="url" class="form-control" value="{{ app.url }}" required>
</div>
<div class="mb-3">
<label class="form-label">图标</label>
<input type="hidden" name="icon" id="selectedIcon" value="{{ app.icon if app.icon else '' }}" required>
<div class="d-flex align-items-center mb-3">
<div class="icon-preview me-3">
<i id="iconPreview" class="{% if app.icon %}{{ app.icon }}{% else %}fas fa-question-circle{% endif %} fa-2x"></i>
</div>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#iconModal">
<i class="fas fa-icons me-2"></i>选择图标
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">主分类</label>
<select name="main_category" id="main_category" class="form-select" required>
<option value="">请选择</option>
{% for main_id, cat in categories.items() %}
<option value="{{ main_id }}" {% if app.category.main == main_id %}selected{% endif %}>{{ cat.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">子分类</label>
<select name="sub_category" id="sub_category" class="form-select" required>
{% if app.category.main in categories %}
{% for sub_id, subData in categories[app.category.main].sub.items() %}
<option value="{{ sub_id }}" {% if app.category.sub == sub_id %}selected{% endif %}>{{ subData.name }}</option>
{% endfor %}
{% else %}
<option value="">请先选择主分类</option>
{% endif %}
</select>
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea name="description" class="form-control" rows="3">{{ app.description if app.description else '' }}</textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" name="private" class="form-check-input" id="privateCheck" {% if app.private %}checked{% endif %}>
<label class="form-check-label" for="privateCheck">私有应用(仅登录后可见)</label>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" class="btn btn-success me-md-2">
<i class="fas fa-save me-2"></i>保存修改
</button>
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>返回
</a>
</div>
</form>
</div>
</div>
<!-- 图标选择模态框 -->
<div class="modal fade" id="iconModal" tabindex="-1" aria-labelledby="iconModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="iconModalLabel">选择图标</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="input-group mb-3">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" id="iconSearch" class="form-control" placeholder="搜索图标...">
</div>
<div class="row row-cols-3 row-cols-sm-4 row-cols-md-5 row-cols-lg-6 row-cols-xl-8 g-3" id="iconGrid">
{% for icon, name in icons.items() %}
<div class="col">
<div class="icon-item p-3 text-center rounded {% if app.icon == icon %}selected{% endif %}"
data-icon="{{ icon }}" title="{{ name }}">
<i class="{{ icon }} fa-2x mb-2"></i>
<div class="small text-truncate">{{ name }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="modal-footer">
<div class="me-auto">
<span id="selectedIconName">
{% if app.icon %}
{{ icons[app.icon] if app.icon in icons else '自定义图标' }}
{% else %}
未选择图标
{% endif %}
</span>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirmIcon">确认选择</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
let selectedIcon = '{{ app.icon if app.icon else "" }}';
let selectedIconName = '{% if app.icon %}{{ icons[app.icon] if app.icon in icons else "自定义图标" }}{% else %}未选择图标{% endif %}';
// 图标搜索功能
document.getElementById('iconSearch').addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const iconItems = document.querySelectorAll('.icon-item');
iconItems.forEach(item => {
const iconName = item.getAttribute('title').toLowerCase();
const iconCode = item.dataset.icon.toLowerCase();
if (iconName.includes(searchTerm) || iconCode.includes(searchTerm)) {
item.closest('.col').style.display = 'block';
} else {
item.closest('.col').style.display = 'none';
}
});
});
// 图标点击选择
document.querySelectorAll('.icon-item').forEach(item => {
item.addEventListener('click', function() {
// 移除所有选中状态
document.querySelectorAll('.icon-item').forEach(i => {
i.classList.remove('selected');
});
// 添加选中状态
this.classList.add('selected');
selectedIcon = this.dataset.icon;
selectedIconName = this.getAttribute('title');
// 更新底部显示
document.getElementById('selectedIconName').textContent = selectedIconName;
});
});
// 确认选择
document.getElementById('confirmIcon').addEventListener('click', function() {
if (selectedIcon) {
document.getElementById('selectedIcon').value = selectedIcon;
document.getElementById('iconPreview').className = selectedIcon + ' fa-2x';
bootstrap.Modal.getInstance(document.getElementById('iconModal')).hide();
} else {
alert('请先选择一个图标');
}
});
// 主分类变化时加载子分类
document.getElementById('main_category').addEventListener('change', function() {
const mainId = this.value;
if (!mainId) return;
fetch('/get_subcategories/' + mainId)
.then(res => res.json())
.then(data => {
const subSelect = document.getElementById('sub_category');
subSelect.innerHTML = '<option value="">请选择</option>';
for (const [id, subData] of Object.entries(data)) {
const option = document.createElement('option');
option.value = id;
option.textContent = subData.name;
subSelect.appendChild(option);
}
// 尝试保留原来的子分类选择
if ('{{ app.category.sub }}' && data['{{ app.category.sub }}']) {
subSelect.value = '{{ app.category.sub }}';
}
});
});
// 初始化时高亮已选图标
if (selectedIcon) {
const selectedItem = document.querySelector(`.icon-item[data-icon="${selectedIcon}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
}
}
});
</script>
<style>
.icon-preview {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border-radius: 5px;
}
.icon-item {
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.icon-item:hover {
background-color: #f8f9fa;
border-color: #dee2e6;
}
.icon-item.selected {
background-color: #e7f1ff;
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
#iconGrid {
max-height: 60vh;
overflow-y: auto;
}
#selectedIconName {
font-weight: bold;
color: #0d6efd;
}
</style>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}编辑主分类{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4>编辑主分类</h4>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label for="main_id" class="form-label">分类ID</label>
<input type="text" class="form-control" id="main_id" value="{{ main_id }}" disabled>
</div>
<div class="mb-3">
<label for="name" class="form-label">分类名称</label>
<input type="text" class="form-control" id="name" name="name" value="{{ category.name }}" required>
</div>
<div class="mb-3">
<label for="color" class="form-label">颜色</label>
<input type="color" name="color" id="color" class="form-control form-control-color"
value="{{ category.color }}" title="选择颜色">
</div>
<div class="mb-3">
<label for="weight" class="form-label">权重</label>
<input type="number" class="form-control" id="weight" name="weight" value="{{ category.get('weight', 0) }}">
<small class="text-muted">权重越大,排序越靠前</small>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="private" name="private"
{% if category.get('private', False) %}checked{% endif %}>
<label class="form-check-label" for="private">私有分类</label>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<a href="{{ url_for('manage_categories') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}编辑子分类{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h4>编辑子分类</h4>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label for="sub_id" class="form-label">子分类ID</label>
<input type="text" class="form-control" id="sub_id" name="sub_id" value="{{ sub_id }}" required>
</div>
<div class="mb-3">
<label for="sub_name" class="form-label">子分类名称</label>
<input type="text" class="form-control" id="sub_name" name="sub_name" value="{{ sub_name }}" required>
</div>
<div class="mb-3">
<label for="color" class="form-label">颜色</label>
<input type="color" name="color" id="color" class="form-control form-control-color"
value="{{ color }}" title="选择颜色">
</div>
<div class="mb-3">
<label for="weight" class="form-label">权重</label>
<input type="number" class="form-control" id="weight" name="weight" value="{{ weight }}">
<small class="text-muted">权重越大,排序越靠前</small>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="private" name="private"
{% if is_private %}checked{% endif %}>
<label class="form-check-label" for="private">私有分类</label>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<a href="{{ url_for('manage_categories') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}

1401
templates/index.html Normal file

File diff suppressed because it is too large Load Diff

68
templates/login.html Normal file
View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<style>
body {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-container {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
}
.login-title {
text-align: center;
margin-bottom: 30px;
}
.captcha-img {
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="login-container">
<h2 class="login-title">应用导航系统</h2>
<!-- 添加这部分代码来显示flash消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="captcha" class="form-label">验证码</label>
<div class="input-group">
<input type="text" class="form-control" id="captcha" name="captcha" required>
<img src="{{ url_for('captcha') }}" class="captcha-img" onclick="this.src='{{ url_for('captcha') }}?'+Math.random()">
</div>
</div>
<button type="submit" class="btn btn-primary w-100">登录</button>
</form>
</div>
</body>
</html>

93
templates/manage.html Normal file
View File

@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}应用管理{% endblock %}
{% block content %}
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4><i class="fas fa-cube me-2"></i>应用管理</h4>
<div>
<a href="{{ url_for('add_app') }}" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> 添加应用
</a>
<a href="{{ url_for('manage_categories') }}" class="btn btn-secondary btn-sm">
<i class="fas fa-tags"></i> 管理分类
</a>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<form class="row g-3">
<div class="col-auto">
<select name="category" class="form-select">
<option value="">所有分类</option>
{% for cat_id, cat_data in categories.items() %}
<option value="{{ cat_id }}" {% if category_filter == cat_id %}selected{% endif %}>
{{ cat_data.name }} (主分类)
</option>
{% for sub_id, sub_data in cat_data.sub.items() %}
<option value="{{ sub_id }}" {% if category_filter == sub_id %}selected{% endif %}>
&nbsp;&nbsp;&nbsp;{{ sub_data.name }}
</option>
{% endfor %}
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">筛选</button>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-light">
<tr>
<th>图标</th>
<th>标题</th>
<th>分类/权重</th>
<th>URL</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for app in apps %}
<tr>
<td><i class="fas {{ app.icon }} fa-lg"></i></td>
<td>
{{ app.title }}
{% if app.get('private', False) %}
<span class="badge bg-warning text-dark ms-2">私有</span>
{% endif %}
</td>
<td>
<div class="d-flex flex-wrap gap-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>
</td>
<td><a href="{{ app.url }}" target="_blank">{{ app.url[:30] }}...</a></td>
<td>
<a href="{{ url_for('edit_app', index=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('确定删除吗?')">
<i class="fas fa-trash"></i> 删除
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

649
templates/settings.html Normal file
View File

@ -0,0 +1,649 @@
{% extends "base.html" %}
{% block title %}系统设置{% endblock %}
{% block content %}
<div class="card">
<div class="card-header bg-primary text-white">
<h4><i class="fas fa-cog me-2"></i>系统设置</h4>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" class="needs-validation" novalidate>
<!-- 主题设置 -->
<div class="card mb-4 border-light shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-palette me-2"></i>显示设置</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="theme" class="form-label">主题设置</label>
<select name="theme" id="theme" class="form-select" required>
<option value="auto" {% if settings.theme == 'auto' %}selected{% endif %}>自动(跟随系统)</option>
<option value="light" {% if settings.theme == 'light' %}selected{% endif %}>明亮模式</option>
<option value="dark" {% if settings.theme == 'dark' %}selected{% endif %}>暗黑模式</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="card_style" class="form-label">卡片样式</label>
<select name="card_style" id="card_style" class="form-select" required>
<option value="normal" {% if settings.card_style == 'normal' %}selected{% endif %}>正常大小</option>
<option value="compact" {% if settings.card_style == 'compact' %}selected{% endif %}>紧凑模式</option>
</select>
</div>
</div>
</div>
</div>
<!-- 网站标题与Logo -->
<div class="card mb-4 border-light shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-heading me-2"></i>网站标题与Logo</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="site_title" class="form-label">网站标题</label>
<input type="text" class="form-control" id="site_title" name="site_title"
value="{{ settings.site_title }}" required>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="show_logo" name="show_logo"
{% if settings.show_logo %}checked{% endif %}>
<label class="form-check-label" for="show_logo">在标题左侧显示Logo</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Logo类型</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="logo_type" id="logo_type_icon"
value="icon" {% if settings.logo_type == 'icon' %}checked{% endif %}>
<label class="form-check-label" for="logo_type_icon">使用图标</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="logo_type" id="logo_type_image"
value="image" {% if settings.logo_type == 'image' %}checked{% endif %}>
<label class="form-check-label" for="logo_type_image">使用图片</label>
</div>
</div>
<!-- 图标选择 -->
<div id="logoIconSection" class="mb-3" style="{% if settings.logo_type != 'icon' %}display:none;{% endif %}">
<label class="form-label">选择图标</label>
<div class="input-group">
<span class="input-group-text"><i class="fas {{ settings.logo_icon }}"></i></span>
<select class="form-select" name="logo_icon" id="logo_icon">
{% for icon, name in icons.items() %}
<option value="{{ icon }}" {% if icon.endswith(settings.logo_icon) %}selected{% endif %}>
{{ name }} ({{ icon }})
</option>
{% endfor %}
</select>
</div>
</div>
<!-- 图片选择 -->
<div id="logoImageSection" style="{% if settings.logo_type != 'image' %}display:none;{% endif %}">
<div class="mb-3">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="logo_image_type" id="logo_image_none" value="none"
{% if not settings.logo_image %}checked{% endif %}>
<label class="form-check-label" for="logo_image_none">使用默认图片</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="logo_image_type" id="logo_image_existing" value="existing"
{% if settings.logo_image %}checked{% endif %}>
<label class="form-check-label" for="logo_image_existing">选择已上传的Logo</label>
<input type="hidden" name="selected_logo" id="selected_logo" value="{{ settings.logo_image.split('/')[-1] if settings.logo_image else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" data-bs-toggle="modal" data-bs-target="#logoModal">
<i class="fas fa-image me-1"></i>选择Logo
</button>
</div>
</div>
{% if settings.logo_image and settings.logo_type == 'image' %}
<div class="current-logo mt-3">
<p class="mb-1"><strong>当前Logo预览:</strong></p>
<img src="{{ settings.logo_image }}" style="max-width: 100px; max-height: 100px;" class="img-thumbnail">
</div>
{% endif %}
</div>
</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-sun me-2"></i>明亮模式背景</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="bg_type" id="bg_none" value="none"
{% if settings.bg_image == 'none' %}checked{% endif %}>
<label class="form-check-label" for="bg_none">不使用背景</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="bg_type" id="bg_default" value="default"
{% if settings.bg_image == '/static/background_light.jpg' or (not settings.bg_image or settings.bg_image == '') %}checked{% endif %}>
<label class="form-check-label" for="bg_default">使用默认明亮背景</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="bg_type" id="bg_existing" value="existing"
{% if settings.bg_image and settings.bg_image != '/static/background_light.jpg' and settings.bg_image != 'none' and not settings.bg_image.endswith('.mp4') %}checked{% endif %}>
<label class="form-check-label" for="bg_existing">选择已上传的背景图片</label>
<input type="hidden" name="selected_bg" id="selected_bg" value="{{ settings.bg_image.split('/')[-1] if settings.bg_image and settings.bg_image != '/static/background_light.jpg' and settings.bg_image != 'none' and not settings.bg_image.endswith('.mp4') else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" data-bs-toggle="modal" data-bs-target="#bgModal">
<i class="fas fa-image me-1"></i>选择背景图片
</button>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="bg_type" id="bg_video" value="video"
{% if settings.bg_image and settings.bg_image.endswith('.mp4') %}checked{% endif %}>
<label class="form-check-label" for="bg_video">选择已上传的背景视频</label>
<input type="hidden" name="selected_video" id="selected_video" value="{{ settings.bg_image.split('/')[-1] if settings.bg_image and settings.bg_image.endswith('.mp4') else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" data-bs-toggle="modal" data-bs-target="#videoModal">
<i class="fas fa-video me-1"></i>选择背景视频
</button>
</div>
</div>
{% if settings.bg_image and settings.bg_image != '/static/background_light.jpg' and settings.bg_image != 'none' %}
<div class="current-bg mt-3">
<p class="mb-1"><strong>当前背景预览:</strong></p>
{% if settings.bg_image.endswith('.mp4') %}
<video width="300" height="150" controls class="img-thumbnail">
<source src="{{ settings.bg_image }}" type="video/mp4">
您的浏览器不支持视频预览
</video>
{% else %}
<img src="{{ settings.bg_image }}" style="max-width: 300px; max-height: 150px;" class="img-thumbnail">
{% endif %}
</div>
{% endif %}
</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-moon me-2"></i>暗黑模式背景</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="dark_bg_type" id="dark_bg_none" value="none"
{% if settings.dark_bg_image == 'none' %}checked{% endif %}>
<label class="form-check-label" for="dark_bg_none">不使用背景</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="dark_bg_type" id="dark_bg_default" value="default"
{% if settings.dark_bg_image == '/static/background_dark.jpg' or (not settings.dark_bg_image or settings.dark_bg_image == '') %}checked{% endif %}>
<label class="form-check-label" for="dark_bg_default">使用默认暗黑背景</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="dark_bg_type" id="dark_bg_existing" value="existing"
{% if settings.dark_bg_image and settings.dark_bg_image != '/static/background_dark.jpg' and settings.dark_bg_image != 'none' and not settings.dark_bg_image.endswith('.mp4') %}checked{% endif %}>
<label class="form-check-label" for="dark_bg_existing">选择已上传的背景图片</label>
<input type="hidden" name="selected_dark_bg" id="selected_dark_bg" value="{{ settings.dark_bg_image.split('/')[-1] if settings.dark_bg_image and settings.dark_bg_image != '/static/background_dark.jpg' and settings.dark_bg_image != 'none' and not settings.dark_bg_image.endswith('.mp4') else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" data-bs-toggle="modal" data-bs-target="#darkBgModal">
<i class="fas fa-image me-1"></i>选择背景图片
</button>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="dark_bg_type" id="dark_bg_video" value="video"
{% if settings.dark_bg_image and settings.dark_bg_image.endswith('.mp4') %}checked{% endif %}>
<label class="form-check-label" for="dark_bg_video">选择已上传的背景视频</label>
<input type="hidden" name="selected_dark_video" id="selected_dark_video" value="{{ settings.dark_bg_image.split('/')[-1] if settings.dark_bg_image and settings.dark_bg_image.endswith('.mp4') else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" data-bs-toggle="modal" data-bs-target="#darkVideoModal">
<i class="fas fa-video me-1"></i>选择背景视频
</button>
</div>
</div>
{% if settings.dark_bg_image and settings.dark_bg_image != '/static/background_dark.jpg' and settings.dark_bg_image != 'none' %}
<div class="current-bg mt-3">
<p class="mb-1"><strong>当前背景预览:</strong></p>
{% if settings.dark_bg_image.endswith('.mp4') %}
<video width="300" height="150" controls class="img-thumbnail">
<source src="{{ settings.dark_bg_image }}" type="video/mp4">
您的浏览器不支持视频预览
</video>
{% else %}
<img src="{{ settings.dark_bg_image }}" style="max-width: 300px; max-height: 150px;" class="img-thumbnail">
{% endif %}
</div>
{% endif %}
</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-lock me-2"></i>安全设置</h5>
</div>
<div class="card-body">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<i class="fas fa-key me-2"></i>修改管理员密码
</div>
<div class="card-body">
<div class="mb-3">
<label for="old_password" class="form-label">当前密码</label>
<input type="password" class="form-control" id="old_password" name="old_password">
</div>
<div class="mb-3">
<label for="new_password" class="form-label">新密码</label>
<input type="password" class="form-control" id="new_password" name="new_password">
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password">
</div>
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle me-2"></i>如果不修改密码,请留空这些字段
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i>返回首页
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>保存设置
</button>
</div>
</form>
</div>
</div>
<!-- Logo选择模态框 -->
<div class="modal fade" id="logoModal" tabindex="-1" aria-labelledby="logoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="logoModalLabel">选择Logo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
{% for attachment in attachments if attachment.type == 'logo' %}
<div class="col-md-3 mb-3">
<div class="card h-100 cursor-pointer" onclick="selectLogo('{{ attachment.filename }}')">
<img src="{{ url_for('uploaded_logo', filename=attachment.filename) }}"
class="card-img-top img-thumbnail"
style="height: 100px; object-fit: contain;">
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<!-- 明亮模式背景图片选择模态框 -->
<div class="modal fade" id="bgModal" tabindex="-1" aria-labelledby="bgModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bgModalLabel">选择明亮模式背景图片</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
{% for attachment in attachments if attachment.type == 'background' %}
<div class="col-md-4 mb-3">
<div class="card h-100 cursor-pointer" onclick="selectBg('{{ attachment.filename }}')">
<img src="{{ url_for('uploaded_background', filename=attachment.filename) }}"
class="card-img-top img-thumbnail"
style="height: 150px; object-fit: cover;">
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<!-- 明亮模式背景视频选择模态框 -->
<div class="modal fade" id="videoModal" tabindex="-1" aria-labelledby="videoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="videoModalLabel">选择明亮模式背景视频</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
{% for attachment in attachments if attachment.type == 'video' %}
<div class="col-md-6 mb-3">
<div class="card h-100 cursor-pointer" onclick="selectVideo('{{ attachment.filename }}')">
<video class="card-img-top img-thumbnail" style="height: 150px; object-fit: cover;" muted>
<source src="{{ url_for('uploaded_video', filename=attachment.filename) }}" type="video/mp4">
</video>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<!-- 暗黑模式背景图片选择模态框 -->
<div class="modal fade" id="darkBgModal" tabindex="-1" aria-labelledby="darkBgModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="darkBgModalLabel">选择暗黑模式背景图片</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
{% for attachment in attachments if attachment.type == 'background' %}
<div class="col-md-4 mb-3">
<div class="card h-100 cursor-pointer" onclick="selectDarkBg('{{ attachment.filename }}')">
<img src="{{ url_for('uploaded_background', filename=attachment.filename) }}"
class="card-img-top img-thumbnail"
style="height: 150px; object-fit: cover;">
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<!-- 暗黑模式背景视频选择模态框 -->
<div class="modal fade" id="darkVideoModal" tabindex="-1" aria-labelledby="darkVideoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="darkVideoModalLabel">选择暗黑模式背景视频</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
{% for attachment in attachments if attachment.type == 'video' %}
<div class="col-md-6 mb-3">
<div class="card h-100 cursor-pointer" onclick="selectDarkVideo('{{ attachment.filename }}')">
<video class="card-img-top img-thumbnail" style="height: 150px; object-fit: cover;" muted>
<source src="{{ url_for('uploaded_video', filename=attachment.filename) }}" type="video/mp4">
</video>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
</div>
</div>
</div>
</div>
<script>
// 全局选择函数
function selectLogo(filename) {
document.getElementById('selected_logo').value = filename;
document.getElementById('logo_image_existing').checked = true;
document.getElementById('logo_image_none').checked = false;
// 更新预览
const previewContainer = document.querySelector('.current-logo');
if (!previewContainer) {
// 如果预览容器不存在,创建一个
const logoImageSection = document.getElementById('logoImageSection');
const previewDiv = document.createElement('div');
previewDiv.className = 'current-logo mt-3';
previewDiv.innerHTML = `
<p class="mb-1"><strong>当前Logo预览:</strong></p>
<img src="/upload/logo/${filename}" style="max-width: 100px; max-height: 100px;" class="img-thumbnail">
`;
logoImageSection.appendChild(previewDiv);
} else {
// 更新现有预览
previewContainer.querySelector('img').src = `/upload/logo/${filename}`;
}
// 使用原生 JavaScript 关闭模态框
const logoModal = bootstrap.Modal.getInstance(document.getElementById('logoModal'));
logoModal.hide();
}
function selectBg(filename) {
document.getElementById('selected_bg').value = filename;
document.getElementById('bg_existing').checked = true;
document.getElementById('bg_none').checked = false;
document.getElementById('bg_default').checked = false;
document.getElementById('bg_video').checked = false;
// 更新预览
const previewContainer = document.querySelector('.current-bg');
if (!previewContainer) {
const bgSection = document.querySelector('input[name="bg_type"]').closest('.card');
const previewDiv = document.createElement('div');
previewDiv.className = 'current-bg mt-3';
previewDiv.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<img src="/upload/background/${filename}" style="max-width: 300px; max-height: 150px;" class="img-thumbnail">
`;
bgSection.querySelector('.card-body').appendChild(previewDiv);
} else {
previewContainer.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<img src="/upload/background/${filename}" style="max-width: 300px; max-height: 150px;" class="img-thumbnail">
`;
}
const bgModal = bootstrap.Modal.getInstance(document.getElementById('bgModal'));
bgModal.hide();
}
function selectVideo(filename) {
document.getElementById('selected_video').value = filename;
document.getElementById('bg_video').checked = true;
document.getElementById('bg_none').checked = false;
document.getElementById('bg_default').checked = false;
document.getElementById('bg_existing').checked = false;
// 更新预览
const previewContainer = document.querySelector('.current-bg');
if (!previewContainer) {
const bgSection = document.querySelector('input[name="bg_type"]').closest('.card');
const previewDiv = document.createElement('div');
previewDiv.className = 'current-bg mt-3';
previewDiv.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<video width="300" height="150" controls class="img-thumbnail">
<source src="/upload/video/${filename}" type="video/mp4">
您的浏览器不支持视频预览
</video>
`;
bgSection.querySelector('.card-body').appendChild(previewDiv);
} else {
previewContainer.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<video width="300" height="150" controls class="img-thumbnail">
<source src="/upload/video/${filename}" type="video/mp4">
您的浏览器不支持视频预览
</video>
`;
}
const videoModal = bootstrap.Modal.getInstance(document.getElementById('videoModal'));
videoModal.hide();
}
function selectDarkBg(filename) {
document.getElementById('selected_dark_bg').value = filename;
document.getElementById('dark_bg_existing').checked = true;
document.getElementById('dark_bg_none').checked = false;
document.getElementById('dark_bg_default').checked = false;
document.getElementById('dark_bg_video').checked = false;
const previewContainer = document.querySelectorAll('.current-bg')[1];
if (!previewContainer) {
const darkBgSection = document.querySelector('input[name="dark_bg_type"]').closest('.card');
const previewDiv = document.createElement('div');
previewDiv.className = 'current-bg mt-3';
previewDiv.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<img src="/upload/background/${filename}" style="max-width: 300px; max-height: 150px;" class="img-thumbnail">
`;
darkBgSection.querySelector('.card-body').appendChild(previewDiv);
} else {
previewContainer.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<img src="/upload/background/${filename}" style="max-width: 300px; max-height: 150px;" class="img-thumbnail">
`;
}
const darkBgModal = bootstrap.Modal.getInstance(document.getElementById('darkBgModal'));
darkBgModal.hide();
}
function selectDarkVideo(filename) {
document.getElementById('selected_dark_video').value = filename;
document.getElementById('dark_bg_video').checked = true;
document.getElementById('dark_bg_none').checked = false;
document.getElementById('dark_bg_default').checked = false;
document.getElementById('dark_bg_existing').checked = false;
const previewContainer = document.querySelectorAll('.current-bg')[1];
if (!previewContainer) {
const darkBgSection = document.querySelector('input[name="dark_bg_type"]').closest('.card');
const previewDiv = document.createElement('div');
previewDiv.className = 'current-bg mt-3';
previewDiv.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<video width="300" height="150" controls class="img-thumbnail">
<source src="/upload/video/${filename}" type="video/mp4">
您的浏览器不支持视频预览
</video>
`;
darkBgSection.querySelector('.card-body').appendChild(previewDiv);
} else {
previewContainer.innerHTML = `
<p class="mb-1"><strong>当前背景预览:</strong></p>
<video width="300" height="150" controls class="img-thumbnail">
<source src="/upload/video/${filename}" type="video/mp4">
您的浏览器不支持视频预览
</video>
`;
}
const darkVideoModal = bootstrap.Modal.getInstance(document.getElementById('darkVideoModal'));
darkVideoModal.hide();
}
document.addEventListener('DOMContentLoaded', function() {
// 明亮模式背景
const bgRadios = document.querySelectorAll('input[name="bg_type"]');
bgRadios.forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById('selected_bg').disabled = this.value !== 'existing';
document.getElementById('selected_video').disabled = this.value !== 'video';
});
});
// 暗黑模式背景
const darkBgRadios = document.querySelectorAll('input[name="dark_bg_type"]');
darkBgRadios.forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById('selected_dark_bg').disabled = this.value !== 'existing';
document.getElementById('selected_dark_video').disabled = this.value !== 'video';
});
});
// Logo图片类型
const logoImageRadios = document.querySelectorAll('input[name="logo_image_type"]');
logoImageRadios.forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById('selected_logo').disabled = this.value !== 'existing';
});
});
// 动态显示/隐藏logo设置部分
document.querySelectorAll('input[name="logo_type"]').forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById('logoIconSection').style.display = this.value === 'icon' ? 'block' : 'none';
document.getElementById('logoImageSection').style.display = this.value === 'image' ? 'block' : 'none';
});
});
// 表单验证
const forms = document.querySelectorAll('.needs-validation');
forms.forEach(form => {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
});
</script>
<style>
.card-header {
font-weight: 600;
}
.form-check-label {
font-weight: 500;
}
.current-bg img, .current-logo img, .current-bg video {
border: 2px solid #dee2e6;
}
.img-thumbnail {
background-color: #f8f9fa;
}
.cursor-pointer {
cursor: pointer;
}
.cursor-pointer:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
transition: all 0.2s ease-in-out;
}
.card .card-header {
border-bottom: 1px solid rgba(0,0,0,.125);
}
.card .card-body {
padding: 1.5rem;
}
.card .card-header.bg-light {
background-color: #abcbeb !important;
}
.card .card-header h5 {
font-size: 1.1rem;
color: #495057;
}
.card.mb-4 {
margin-bottom: 1.5rem!important;
}
</style>
{% endblock %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Binary file not shown.