根因:此前为移动端手写的一段折叠逻辑直接给 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 一行配置。
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""
|
||
移动端侧栏增强插件入口
|
||
|
||
背景
|
||
----
|
||
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)")
|