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/') def uploaded_video(filename): return send_from_directory(VIDEO_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]: 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') 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/', 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/') @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() # 检查登录状态 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)