From b0865ea18d8226e4df8d9307f9c4711345433100 Mon Sep 17 00:00:00 2001
From: bwstudio
Date: Wed, 9 Sep 2026 19:20:03 +0800
Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E4=BF=AE=E5=A4=8D=E7=A7=BB?=
=?UTF-8?q?=E5=8A=A8=E7=AB=AF=E5=90=8E=E5=8F=B0=E4=BE=A7=E6=A0=8F=E9=94=99?=
=?UTF-8?q?=E4=BD=8D=E4=B8=8E=E5=AD=90=E8=8F=9C=E5=8D=95=E4=B8=8D=E6=94=B6?=
=?UTF-8?q?=E8=B5=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
根因:此前为移动端手写的一段折叠逻辑直接给 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 一行配置。
---
applications/config.py | 2 +-
plugins/mobileUI/README.md | 54 +++++++++++
plugins/mobileUI/__init__.json | 5 +
plugins/mobileUI/__init__.py | 75 +++++++++++++++
plugins/mobileUI/static/mobile.js | 148 ++++++++++++++++++++++++++++++
templates/system/common/head.html | 26 ------
templates/system/common/side.html | 74 ---------------
templates/system/index.html | 34 -------
8 files changed, 283 insertions(+), 135 deletions(-)
create mode 100644 plugins/mobileUI/README.md
create mode 100644 plugins/mobileUI/__init__.json
create mode 100644 plugins/mobileUI/__init__.py
create mode 100644 plugins/mobileUI/static/mobile.js
delete mode 100644 templates/system/common/head.html
delete mode 100644 templates/system/common/side.html
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"
-
-
@@ -153,32 +145,6 @@
// 渲染
admin.render();
- // 移动端初始化:窗口较窄时默认折叠左侧菜单,避免内容被侧边栏挡住
- (function () {
- function foldSidebar() {
- if ($(window).width() > 768) return;
- var body = $('body');
- var admin = $('.pear-admin');
- // 直接加 class:不依赖 .collapse 按钮是否存在或是否响应点击
- if (!admin.hasClass('pear-mini')) {
- admin.addClass('pear-mini');
- body.addClass('pear-mini');
- }
- // 同步 menu 实例状态,防止后续交互异常
- if (window.PearAdmin && window.PearAdmin.instances && window.PearAdmin.instances.menu) {
- try {
- window.PearAdmin.instances.menu.collapse();
- } catch (e) {}
- window.PearAdmin.instances.menu.isCollapse = true;
- }
- }
- // 立即 + 多次延迟:覆盖 admin.js 异步初始化和菜单渲染滞后
- foldSidebar();
- setTimeout(foldSidebar, 300);
- setTimeout(foldSidebar, 800);
- setTimeout(foldSidebar, 1500);
- })();
-
// 注销
admin.logout(function () {
let loading = layer.load()