#!/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/") @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