1187 lines
42 KiB
Python
1187 lines
42 KiB
Python
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') |