AIDaohang/app.py

1187 lines
42 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
from math import ceil
import argparse
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)
ICON_FOLDER = os.path.join(UPLOAD_FOLDER, 'icon')
os.makedirs(ICON_FOLDER, exist_ok=True)
FONT_PATH = os.path.join(BASE_DIR, 'static', 'webfonts', 'arial.ttf')
# 允许的文件扩展名
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')
GUEST_SETTINGS_FILE = os.path.join(DATA_DIR, 'guest_settings.json')
# 主题设置文件路径
THEME_SETTINGS_FILE = os.path.join(DATA_DIR, 'theme_settings.json')
def load_theme_settings():
"""加载主题设置"""
default_settings = {
'light': {
'primary_color': '#4361ee',
'title_color': '#4361ee',
'opacity': 0.3,
'bg_color': '#ffffff'
},
'dark': {
'primary_color': '#4361ee',
'title_color': '#4361ee',
'opacity': 0.7,
'bg_color': '#1e1e1e'
}
}
try:
if os.path.exists(THEME_SETTINGS_FILE):
with open(THEME_SETTINGS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return default_settings
except Exception as e:
print(f"加载主题设置失败: {e}")
return default_settings
def save_theme_settings(settings):
"""保存主题设置"""
try:
with open(THEME_SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=4)
except Exception as e:
print(f"保存主题设置失败: {e}")
def migrate_settings(settings):
"""迁移旧版设置到新版格式"""
if 'admin_password' in settings:
# 如果存在旧版明文密码转换为哈希使用pbkdf2:sha256方法
settings['admin_password_hash'] = generate_password_hash(
settings['admin_password'],
method='pbkdf2:sha256'
)
del settings['admin_password']
elif 'admin_password_hash' not in settings:
# 如果既没有旧版密码也没有哈希,生成默认密码哈希
settings['admin_password_hash'] = generate_password_hash(
'123456',
method='pbkdf2:sha256'
)
# 确保有页脚设置
if 'footer_html' not in settings:
settings[
'footer_html'] = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
return settings
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": [],
"footer_html": '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
}
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(default_settings, f, ensure_ascii=False, indent=2)
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)
# 添加新字段的默认值
if 'footer_html' not in settings:
settings['footer_html'] = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
elif 'footer_html' not in settings:
# 已有密码哈希但没有页脚设置的情况
settings['footer_html'] = '<div class="flex justify-center text-slate-300" style="margin-top:100px">Powered By <a href="https://github.com" target="_blank" class="ml-[5px]">AIDaohang</a></div>'
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
init_settings()
def init_guest_settings():
"""初始化游客设置文件"""
if not os.path.exists(GUEST_SETTINGS_FILE):
default_guest_settings = {
"theme": "auto",
"card_style": "normal",
"bg_image": "none",
"dark_bg_image": "none"
}
with open(GUEST_SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(default_guest_settings, f, ensure_ascii=False, indent=2)
init_guest_settings()
def load_guest_settings():
"""加载游客设置"""
with open(GUEST_SETTINGS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def save_guest_settings(data):
"""保存游客设置"""
with open(GUEST_SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
@app.route('/api/guest_settings', methods=['GET', 'POST'])
def handle_guest_settings():
if request.method == 'POST':
if 'username' not in session:
return jsonify({'error': 'Unauthorized'}), 401
data = request.json
save_guest_settings(data)
return jsonify({'status': 'success'})
else:
return jsonify(load_guest_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()
file_size = 0
for folder in [LOGO_FOLDER, BACKGROUND_FOLDER, VIDEO_FOLDER, ICON_FOLDER]:
filepath = os.path.join(folder, filename)
if os.path.exists(filepath):
file_size = os.path.getsize(filepath)
break
attachments.append({
'filename': filename,
'type': file_type,
'upload_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'size': file_size
})
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):
# 生成12位随机字符串作为文件名
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
random_str = ''.join(random.choice(chars) for _ in range(12))
return f"{random_str}.{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('navigation')) # 默认跳转到首页
# 移除flash消息因为登录成功不需要显示
return redirect(next_url)
flash('用户名或密码错误', 'danger')
return render_template('login.html')
# 获取next参数并传递给模板
next_page = request.args.get('next', '/')
return render_template('login.html', next=next_page)
@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(FONT_PATH, 24)
except:
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('/api/theme_settings', methods=['GET', 'POST'])
def handle_theme_settings():
if request.method == 'POST':
if 'username' not in session:
return jsonify({'error': 'Unauthorized'}), 401
data = request.json
# 验证数据格式
required_fields = ['light', 'dark']
for mode in required_fields:
if mode not in data:
return jsonify({'error': f'Missing {mode} settings'}), 400
for field in ['primary_color', 'title_color', 'opacity', 'bg_color']:
if field not in data[mode]:
return jsonify({'error': f'Missing {field} in {mode} settings'}), 400
# 保存设置
save_theme_settings(data)
return jsonify({'status': 'success'})
# GET请求返回当前主题设置
return jsonify(load_theme_settings())
@app.route('/settings', methods=['GET', 'POST'])
@login_required
def system_settings():
settings = load_settings()
theme_settings = load_theme_settings()
guest_settings = load_guest_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')
settings['footer_html'] = request.form.get('footer_html', '')
# 处理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"
# 更新游客设置
guest_settings['theme'] = request.form.get('guest_theme', 'auto')
guest_settings['card_style'] = request.form.get('guest_card_style', 'normal')
# 处理游客背景设置
guest_bg_type = request.form.get('guest_bg_type')
if guest_bg_type == 'none':
guest_settings['bg_image'] = 'none'
elif guest_bg_type == 'default':
guest_settings['bg_image'] = '/static/background_light.jpg'
else: # system
guest_settings['bg_image'] = settings.get('bg_image', '/static/background_light.jpg')
guest_dark_bg_type = request.form.get('guest_dark_bg_type')
if guest_dark_bg_type == 'none':
guest_settings['dark_bg_image'] = 'none'
elif guest_dark_bg_type == 'default':
guest_settings['dark_bg_image'] = '/static/background_dark.jpg'
else: # system
guest_settings['dark_bg_image'] = settings.get('dark_bg_image', '/static/background_dark.jpg')
save_guest_settings(guest_settings)
# 检查是否修改密码
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_to_return, theme_settings=theme_settings, guest_settings=guest_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,
theme_settings=theme_settings,
guest_settings=guest_settings,
icons=load_icons(),
attachments=attachments)
# 添加附件管理路由
@app.route('/attachments')
@login_required
def manage_attachments():
type_filter = request.args.get('type', 'all')
search_query = request.args.get('search', '').lower()
sort_order = request.args.get('sort', 'desc') # 默认按时间倒序
page = int(request.args.get('page', 1))
per_page = 10 # 每页显示10条数据
settings = load_settings()
attachments = load_attachments()
# 添加文件大小信息
for attachment in attachments:
file_found = False
for folder in [LOGO_FOLDER, BACKGROUND_FOLDER, VIDEO_FOLDER, ICON_FOLDER]:
filepath = os.path.join(folder, attachment['filename'])
if os.path.exists(filepath):
attachment['size'] = os.path.getsize(filepath)
file_found = True
break
if not file_found:
attachment['size'] = 0
# 应用筛选
filtered_attachments = []
for attachment in attachments:
# 类型筛选
if type_filter != 'all' and attachment['type'] != type_filter:
continue
# 搜索筛选
if search_query and search_query not in attachment['filename'].lower():
continue
filtered_attachments.append(attachment)
# 排序处理
filtered_attachments.sort(
key=lambda x: x['upload_time'],
reverse=(sort_order == 'desc')
)
# 分页处理
total_pages = ceil(len(filtered_attachments) / per_page)
paginated_attachments = filtered_attachments[(page - 1) * per_page: page * per_page]
return render_template('attachments.html',
settings=settings,
attachments=paginated_attachments,
current_page=page,
total_pages=total_pages,
search_query=search_query,
type_filter=type_filter,
sort_order=sort_order)
@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
elif file_type == 'video':
folder = VIDEO_FOLDER
else: # icon
folder = ICON_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('/upload/icon/<filename>')
def uploaded_icon(filename):
return send_from_directory(ICON_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, VIDEO_FOLDER, ICON_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')
search_query = request.args.get('search', '').lower()
page = int(request.args.get('page', 1))
per_page = 10 # 每页显示10条数据
apps = load_apps()
categories = load_categories()
settings = load_settings()
# 应用筛选
filtered_apps = []
for app in apps:
# 分类筛选
if category_filter and not (app['category']['main'] == category_filter or
app['category']['sub'] == category_filter or
(category_filter in categories and app['category']['main'] == category_filter)):
continue
# 搜索筛选
if search_query and not (search_query in app['title'].lower() or
search_query in app['url'].lower() or
search_query in app.get('description', '').lower() or
search_query in app['category']['main'].lower() or
search_query in app['category']['sub'].lower()):
continue
filtered_apps.append(app)
# 分页处理
total_pages = ceil(len(filtered_apps) / per_page)
paginated_apps = filtered_apps[(page - 1) * per_page: page * per_page]
return render_template('manage.html',
apps=paginated_apps,
settings=settings,
categories=categories,
current_page=page,
total_pages=total_pages,
per_page=per_page,
search_query=search_query,
category_filter=category_filter)
@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()
attachments = load_attachments()
if request.method == 'POST':
title = request.form['title']
url = request.form['url']
icon = request.form['icon']
description = request.form.get('description', '')
private = 'private' in request.form
main_category = request.form['main_category']
sub_category = request.form['sub_category']
new_app = {
"title": title,
"url": url,
"icon": icon,
"description": description,
"private": private,
"category": {
"main": main_category,
"sub": sub_category
}
}
apps = load_apps()
apps.append(new_app)
save_apps(apps)
return redirect(url_for('index'))
return render_template('add_app.html',
categories=categories,
settings=settings,
icons=load_icons(),
attachments=attachments) # 添加attachments参数
@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()
attachments = load_attachments()
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, attachments=attachments)
@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()
categories = load_categories()
# 检查登录状态
is_logged_in = 'username' in session
# 如果未登录,过滤掉私有应用和私有分类中的应用
if not is_logged_in:
filtered_apps = []
for app in apps:
# 跳过私有应用
if app.get('private', False):
continue
main_cat = app['category']['main']
sub_cat = app['category']['sub']
# 检查主分类是否为私有
if main_cat in categories and categories[main_cat].get('private', False):
continue
# 检查子分类是否为私有
if (main_cat in categories and
'sub_private' in categories[main_cat] and
sub_cat in categories[main_cat]['sub_private'] and
categories[main_cat]['sub_private'][sub_cat]):
continue
filtered_apps.append(app)
return jsonify(filtered_apps)
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,
'weight': main_data.get('weight', 0) # 保留权重信息
}
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)
def change_password(username, new_password):
settings = load_settings()
if username == 'admin':
settings['admin_password_hash'] = generate_password_hash(new_password, method='pbkdf2:sha256')
save_settings(settings)
print(f"管理员 {username} 的密码已成功修改!")
else:
print("错误:只能修改管理员 (admin) 的密码")
@app.route('/app/add_from_category', methods=['POST'])
@login_required
def add_app_from_category():
categories = load_categories()
icons = load_icons()
settings = load_settings()
attachments = load_attachments()
if request.method == 'POST':
title = request.form['title']
url = request.form['url']
icon = request.form['icon']
description = request.form.get('description', '')
private = 'private' in request.form
main_category = request.form['main_category']
sub_category = request.form['sub_category']
new_app = {
"title": title,
"url": url,
"icon": icon,
"description": description,
"private": private,
"category": {
"main": main_category,
"sub": sub_category
}
}
apps = load_apps()
apps.append(new_app)
save_apps(apps)
flash('应用添加成功', 'success')
return redirect(url_for('manage_categories'))
return render_template('add_app.html',
categories=categories,
settings=settings,
icons=load_icons(),
attachments=attachments)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='导航系统管理工具')
subparsers = parser.add_subparsers(dest='command', help='可用命令')
# 添加 user 子命令
user_parser = subparsers.add_parser('user', help='用户管理')
user_parser.add_argument('username', help='用户名')
user_parser.add_argument('password', help='新密码')
args = parser.parse_args()
if args.command == 'user':
change_password(args.username, args.password)
else:
# 如果没有指定命令行参数,则运行 Flask 应用
app.run(debug=True, port='8094', host='0.0.0.0')