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, 'data/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'] = '
Powered By AIDaohang
' 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": '
Powered By AIDaohang
' } 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'] = '
Powered By AIDaohang
' 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'] = '
Powered By AIDaohang
' 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/') def uploaded_video(filename): return send_from_directory(VIDEO_FOLDER, filename) @app.route('/upload/icon/') def uploaded_icon(filename): return send_from_directory(ICON_FOLDER, filename) @app.route('/delete_attachment/', 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/') def uploaded_background(filename): return send_from_directory(BACKGROUND_FOLDER, filename) @app.route('/upload/logo/') 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/', 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/') @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/') @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//') @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/') 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/', 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//', 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')