fix(admin): 修复移动端后台侧栏错位与子菜单不收起

根因:此前为移动端手写的一段折叠逻辑直接给 body 加 pear-mini,
绕过了 admin.js 里唯一的同步入口 collapse(),导致三处状态错位——
布局层认为已折叠(侧栏宽 0、菜单文字被 CSS 隐藏),
菜单 DOM 仍是展开态(#side 没有 pear-nav-mini),于是"文字看不见、
但子菜单又在原地铺开"。

处理:
- templates/system/index.html 还原为框架原始版本,删除手写的
  foldSidebar() IIFE 与首屏强制加类逻辑
- 删除无引用的死文件 templates/system/common/{head,side}.html
- 新增插件 plugins/mobileUI,以运行时注入 mobile.js 的方式实现移动端增强:
  * 首屏折叠只通过框架的 li.collapse 点击完成,不手写 class
  * 轮询等待菜单异步渲染就绪后再折叠
  * 补齐 framework 内部状态 isCollapse(后端 /rights/configs 把键拼成了
    "collaspe",admin.js 读 collapse 恒为 undefined,窄屏 resize 会反向展开)
  * 触屏下点击菜单外区域收起 mini 浮层(触屏没有可靠的 mouseleave)
  * 展开态点击叶子菜单后自动收起侧栏
- applications/config.py 的 PLUGIN_ENABLE_FOLDERS 追加 cookieManager、mobileUI

framework 业务逻辑零改动,仅 config.py 一行配置。
This commit is contained in:
bwstudio
2026-09-09 19:20:03 +08:00
parent 3e8c490851
commit b0865ea18d
8 changed files with 283 additions and 135 deletions
+75
View File
@@ -0,0 +1,75 @@
"""
移动端侧栏增强插件入口
背景
----
Pear Admin 的侧栏折叠涉及三处状态,必须同步变化:
1. body / .pear-admin 上的 `pear-mini` —— 控制整页布局(CSS)
2. #side 上的 `pear-nav-mini` —— 控制菜单自身宽度与子菜单呈现方式
3. PearAdmin.instances.menu.isCollapse —— 框架内部状态机
框架只暴露了一个能同时同步这三者的入口:admin.js 里的私有函数 `collapse()`
它由 `body.on("click", ".collapse,.pear-cover", ...)` 驱动。**外面拿不到这个函数**,
所以任何"手动 addClass('pear-mini')"的写法都必然造成状态错位(详见 README)。
本插件的做法
------------
- 不修改 framework 任何文件;
- 通过 after_request 向后台主页注入 `static/mobile.js`
- 脚本里一律用「触发原生 .collapse 点击」的方式折叠,绝不手写 class;
- 额外补两处移动端体验:触屏点空白处收起子菜单浮层、点菜单后自动收起侧栏。
"""
from flask import Blueprint, Flask, request, url_for
bp = Blueprint(
"mobileUI",
__name__,
url_prefix="/plugin/mobileUI",
static_folder="static",
)
def _script_tag() -> bytes:
"""脚本标签的 URL 用 url_for 生成,避免手拼蓝图静态路径出错。"""
tag = f'<script src="{url_for("mobileUI.static", filename="mobile.js")}"></script>'
return tag.encode("utf-8")
def event_init(app: Flask):
"""注册静态资源蓝图(提供 /plugin/mobileUI/static/mobile.js)。"""
app.register_blueprint(bp)
print(" * mobileUI: registered /plugin/mobileUI/static/* blueprint")
def event_finish(app: Flask):
"""在后台主页响应里注入移动端脚本。"""
@app.after_request
def _inject_mobile_script(response):
try:
# 只处理成功的 HTML 页面
if response.status_code != 200:
return response
if not response.content_type.startswith("text/html"):
return response
if response.direct_passthrough:
return response
# 只注入后台主框架页(含侧栏容器 #side),不碰 iframe 子页面与前台页
if request.path not in ("/", "/index", "/system/index"):
return response
body = response.get_data()
if b'id="side"' not in body or b"pear-admin" not in body:
return response
script_tag = _script_tag()
if script_tag in body: # 幂等,避免重复注入
return response
response.set_data(body.replace(b"</body>", script_tag + b"</body>"))
except Exception: # 注入失败绝不能影响正常业务响应
app.logger.exception("mobileUI: inject failed")
return response
print(" * mobileUI: registered after_request hook (inject mobile.js into admin index)")