diff --git a/applications/config.py b/applications/config.py index 28e824e..3afb67f 100644 --- a/applications/config.py +++ b/applications/config.py @@ -118,7 +118,7 @@ class BaseConfig: # ---- 插件 ---- # 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 plugins 方式开发,启用即可挂载路由 - PLUGIN_ENABLE_FOLDERS = ["navManager", "friendManager", "aboutManager", "siteStats"] + PLUGIN_ENABLE_FOLDERS = ["navManager", "friendManager", "aboutManager", "siteStats", "cookieManager", "mobileUI"] class DevConfig(BaseConfig): diff --git a/plugins/mobileUI/README.md b/plugins/mobileUI/README.md new file mode 100644 index 0000000..1ad5110 --- /dev/null +++ b/plugins/mobileUI/README.md @@ -0,0 +1,54 @@ +# mobileUI —— 移动端侧栏增强插件 + +修复手机/窄屏(≤768px)下后台左侧菜单的两个问题: + +1. **菜单错位**:侧栏只剩一列图标、文字全部消失; +2. **子菜单不自动隐藏**:mini 模式下点开的子菜单浮层赖着不走。 + +## 为什么会出问题 + +Pear Admin 的"侧栏折叠"由**三处状态**共同决定: + +| 状态 | 作用 | +|---|---| +| `body`/`.pear-admin` 上的 `pear-mini` | 整页布局:`.layui-side` 压成 0/60px | +| `#side` 上的 `pear-nav-mini` | 菜单自身宽度、子菜单内联展开还是悬浮弹出 | +| `PearAdmin.instances.menu.isCollapse` | 框架状态机,决定下一次 collapse 的方向 | + +框架把三者的同步封装在 admin.js 的**私有函数** `collapse()` 里,只能通过 +点击 `.collapse` / `.pear-cover` 触发。此前在 `templates/system/index.html` +里手写的"首屏 `addClass('pear-mini')`"绕过了它,导致 body 与 #side 状态反相: +布局按折叠渲染(文字被 `.pear-mini ... span {display:none}` 藏掉、侧栏 0px), +菜单却停留在展开态 —— 即"只剩图标""子菜单收不起来"。 + +## 本插件的做法(不改 framework 任何文件) + +- `event_finish` 注册 `after_request`,仅向后台主框架页(含 `#side` 的 HTML) + 注入 `static/mobile.js`; +- 折叠一律**触发框架原生入口**(`li.collapse` 的点击),三态由框架同步; +- 折叠后补写 `menu.isCollapse = true`:后端 `/rights/configs` 里菜单配置的键 + 拼成了 `collaspe`(框架读 `collapse`),done 回调会把它覆写成 `undefined`, + 不补写的话窄屏下任何 resize 都会把侧栏反向弹开; +- 触屏补位:mini 浮层靠 `mouseenter/mouseleave` 开关,而触屏 tap 后 + `mouseleave` 常常不来 —— 点菜单以外区域时主动收起浮层; +- 移动端习惯:展开态点叶子菜单后自动收起侧栏。 + +## 涉及文件 + +``` +plugins/mobileUI/ +├── __init__.json # 插件元信息 +├── __init__.py # after_request 注入 + 静态蓝图 +├── static/mobile.js # 全部前端逻辑 +└── README.md +``` + +## 启用 + +`applications/config.py` → `PLUGIN_ENABLE_FOLDERS` 加入 `"mobileUI"`,重启即可。 + +## 备注 + +配置里 `"collaspe"` 的拼写错误属于框架原文件(`applications/view/system/rights.py`), +按"不改动原项目内容"的原则未修正;插件已在运行时补齐状态,不受影响。 +若未来升级框架,可顺手把键名改为 `"collapse"`。 diff --git a/plugins/mobileUI/__init__.json b/plugins/mobileUI/__init__.json new file mode 100644 index 0000000..1c3586f --- /dev/null +++ b/plugins/mobileUI/__init__.json @@ -0,0 +1,5 @@ +{ + "plugin_name": "移动端侧栏增强", + "plugin_version": "1.0.0", + "plugin_description": "修复移动端(≤768px)后台左侧菜单错位与触屏子菜单浮层不消失的问题。不修改 framework 任何文件,通过 after_request 向后台主页注入脚本,折叠动作一律走框架原生 .collapse 入口,保证 body/#side/isCollapse 三处状态同步。" +} diff --git a/plugins/mobileUI/__init__.py b/plugins/mobileUI/__init__.py new file mode 100644 index 0000000..e332dc2 --- /dev/null +++ b/plugins/mobileUI/__init__.py @@ -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'' + 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"
- -