AIDaohang/app.py
2025-07-06 23:23:23 +08:00

926 lines
33 KiB
Python
Raw 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
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')
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'
)
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 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()
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('navigation')) # 默认跳转到首页
flash('登录成功', 'success')
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('/settings', methods=['GET', 'POST'])
@login_required
def system_settings():
settings = load_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')
# 处理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, 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,
guest_settings=guest_settings,
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
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')
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()
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()
# 检查登录状态
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, port='8094', host='0.0.0.0')