Files
zsglpt/routes/pages.py
yuyx 7007f5f6f5 feat: 完成 Passkey 能力与前后台加载优化
更新说明:\n1. 新增用户端与管理员端 Passkey 登录/注册/设备管理(最多3台,支持设备备注、删除设备)。\n2. 修复 Passkey 注册与登录流程中的浏览器/证书/CSRF相关问题,增强错误提示。\n3. 前台登录页改为独立入口,首屏仅加载必要资源,其他页面按需加载。\n4. 系统配置页改为静默获取金山文档状态,避免首屏阻塞,并优化状态展示为“检测中/已登录/未登录/异常”。\n5. 补充后端接口与页面渲染适配,修复多入口下样式依赖注入问题。\n6. 同步更新前后台构建产物与相关静态资源。
2026-02-15 23:51:46 +08:00

189 lines
6.1 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import json
import os
from typing import Optional
from flask import Blueprint, current_app, redirect, render_template, session, url_for
from flask_login import current_user, login_required
from routes.decorators import admin_required
from services.runtime import get_logger
pages_bp = Blueprint("pages", __name__)
def _collect_entry_css_files(manifest: dict, entry_name: str) -> list[str]:
css_files: list[str] = []
seen_css: set[str] = set()
visited: set[str] = set()
def _append_css(entry_obj: dict) -> None:
for css_file in entry_obj.get("css") or []:
css_path = str(css_file or "").strip()
if not css_path or css_path in seen_css:
continue
seen_css.add(css_path)
css_files.append(css_path)
def _walk_manifest_key(manifest_key: str) -> None:
key = str(manifest_key or "").strip()
if not key or key in visited:
return
visited.add(key)
entry_obj = manifest.get(key)
if not isinstance(entry_obj, dict):
return
_append_css(entry_obj)
for imported_key in entry_obj.get("imports") or []:
_walk_manifest_key(imported_key)
entry = manifest.get(entry_name) or {}
if isinstance(entry, dict):
_append_css(entry)
for imported_key in entry.get("imports") or []:
_walk_manifest_key(imported_key)
return css_files
def render_app_spa_or_legacy(
legacy_template_name: str,
legacy_context: Optional[dict] = None,
spa_initial_state: Optional[dict] = None,
spa_entry_name: str = "index.html",
):
"""渲染前台 Vue SPA构建产物位于 static/app失败则回退旧模板。"""
logger = get_logger()
legacy_context = legacy_context or {}
manifest_path = os.path.join(current_app.root_path, "static", "app", ".vite", "manifest.json")
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = json.load(f)
entry = manifest.get(spa_entry_name) or {}
js_file = entry.get("file")
css_files = _collect_entry_css_files(manifest, spa_entry_name)
if not js_file:
logger.warning(f"[app_spa] manifest缺少入口文件: {manifest_path}")
return render_template(legacy_template_name, **legacy_context)
app_spa_js_file = f"app/{js_file}"
app_spa_css_files = [f"app/{p}" for p in css_files]
app_spa_build_id = _get_asset_build_id(
os.path.join(current_app.root_path, "static"),
[app_spa_js_file, *app_spa_css_files],
)
return render_template(
"app.html",
app_spa_js_file=app_spa_js_file,
app_spa_css_files=app_spa_css_files,
app_spa_build_id=app_spa_build_id,
app_spa_initial_state=spa_initial_state,
)
except FileNotFoundError:
logger.info(f"[app_spa] 未找到manifest: {manifest_path},回退旧模板: {legacy_template_name}")
return render_template(legacy_template_name, **legacy_context)
except Exception as e:
logger.error(f"[app_spa] 加载manifest失败: {e}")
return render_template(legacy_template_name, **legacy_context)
def _get_asset_build_id(static_root: str, rel_paths: list[str]) -> Optional[str]:
mtimes = []
for rel_path in rel_paths:
if not rel_path:
continue
try:
mtimes.append(os.path.getmtime(os.path.join(static_root, rel_path)))
except OSError:
continue
if not mtimes:
return None
return str(int(max(mtimes)))
@pages_bp.route("/")
def index():
"""主页 - 重定向到登录或应用"""
if current_user.is_authenticated:
return redirect(url_for("pages.app_page"))
return redirect(url_for("pages.login_page"))
@pages_bp.route("/login")
def login_page():
"""登录页面"""
return render_app_spa_or_legacy("login.html", spa_entry_name="login.html")
@pages_bp.route("/register")
def register_page():
"""注册页面"""
return render_app_spa_or_legacy("register.html")
@pages_bp.route("/app")
@login_required
def app_page():
"""主应用页面"""
return render_app_spa_or_legacy("index.html")
@pages_bp.route("/app/<path:subpath>")
@login_required
def app_page_subpath(subpath):
"""SPA 子路由刷新支持History 模式)"""
return render_app_spa_or_legacy("index.html")
@pages_bp.route("/yuyx")
def admin_login_page():
"""后台登录页面"""
if "admin_id" in session:
return redirect(url_for("pages.admin_page"))
return render_template("admin_login.html")
@pages_bp.route("/yuyx/admin")
@admin_required
def admin_page():
"""后台管理页面"""
logger = get_logger()
manifest_path = os.path.join(current_app.root_path, "static", "admin", ".vite", "manifest.json")
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = json.load(f)
entry = manifest.get("index.html") or {}
js_file = entry.get("file")
css_files = entry.get("css") or []
if not js_file:
logger.error(f"[admin_spa] manifest缺少入口文件: {manifest_path}")
return "后台前端资源缺失,请重新构建管理端", 503
admin_spa_js_file = f"admin/{js_file}"
admin_spa_css_files = [f"admin/{p}" for p in css_files]
admin_spa_build_id = _get_asset_build_id(
os.path.join(current_app.root_path, "static"),
[admin_spa_js_file, *admin_spa_css_files],
)
return render_template(
"admin.html",
admin_spa_js_file=admin_spa_js_file,
admin_spa_css_files=admin_spa_css_files,
admin_spa_build_id=admin_spa_build_id,
)
except FileNotFoundError:
logger.error(f"[admin_spa] 未找到manifest: {manifest_path}")
return "后台前端资源未构建,请联系管理员", 503
except Exception as e:
logger.error(f"[admin_spa] 加载manifest失败: {e}")
return "后台页面加载失败,请稍后重试", 500