Files
pear-admin-flask/plugins/mobileUI/static/mobile.js
T
bwstudio b0865ea18d 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 一行配置。
2026-09-09 19:20:03 +08:00

149 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 移动端侧栏增强(视口 ≤ 768px)
*
* 为什么要这个脚本
* ----------------
* Pear Admin 的"侧栏折叠"由三处状态共同决定,缺一个就会错位:
*
* 1. body/.pear-admin 上的 `pear-mini` —— 决定整页布局(CSS 把 .layui-side 压成 0/60px
* 2. #side 上的 `pear-nav-mini` —— 决定菜单自身宽度、子菜单是内联展开还是悬浮弹出
* 3. PearAdmin.instances.menu.isCollapse —— 框架内部状态机,决定后续 collapse 的方向
*
* 框架把这三个状态的同步封装在 admin.js 的**私有函数** collapse() 里,
* 而它只由 `body.on("click", ".collapse,.pear-cover")` 这一个委托触发。
* 外部拿不到该函数,因此"手动 addClass('pear-mini')"必然导致 1 和 2 反相:
* 布局说已折叠(侧栏 0px、菜单文字被隐藏),菜单却还停留在展开态,
* 于是出现「侧栏只剩一列图标」「子菜单点开后不收起」的现象。
*
* 本脚本的原则:**折叠一律走框架原生入口**(触发 .collapse 的点击),
* 绝不手写 class,从而保证三处状态永远同步。
*/
(function () {
"use strict";
var MOBILE_MAX = 768;
function isMobile() {
return window.innerWidth <= MOBILE_MAX;
}
function getAdminEl() {
return document.querySelector(".pear-admin");
}
function isCollapsed() {
var el = getAdminEl();
return !!(el && el.classList.contains("pear-mini"));
}
function menuRendered() {
return document.querySelectorAll("#side .layui-nav-item").length > 0;
}
/**
* 折叠/展开侧栏:只触发框架委托的 .collapse 点击。
* 这样 admin.js 的私有 collapse() 会同步 body class、#side 的 pear-nav-mini、
* isCollapse 以及顶栏图标,不会出现状态错位。
*/
function toggleViaFramework() {
var btn = document.querySelector("li.collapse") || document.querySelector(".collapse");
if (!btn) {
return false;
}
btn.click();
return true;
}
/**
* 补齐框架内部状态 isCollapse。
* 后端 /rights/configs 里菜单配置的键拼成了 "collaspe"(框架读 "collapse"),
* 因此 admin.js 的 done 回调会把 isCollapse 覆写为 undefinedfalsy)。
* 折叠后若 isCollapse 不为 true,窄屏下任何 resize 都会触发框架的反向
* collapse,把已经折叠的侧栏重新弹开 —— 这里把状态补齐。
*/
function patchCollapseState() {
try {
var m = window.PearAdmin && window.PearAdmin.instances && window.PearAdmin.instances.menu;
if (m) {
m.isCollapse = true;
}
} catch (e) { /* 忽略:状态补齐失败不影响布局正确性 */ }
}
// ---------------------------------------------------------------------
// 1) 首屏折叠:菜单是异步渲染的,轮询等它就绪后再确认折叠(幂等)
// ---------------------------------------------------------------------
(function foldOnStart() {
var tries = 0;
var timer = setInterval(function () {
if (++tries > 40) { // 最多等约 6 秒,避免无限轮询
clearInterval(timer);
return;
}
if (!isMobile() || !menuRendered()) {
return;
}
if (isCollapsed()) { // 框架自己(resize 兼容逻辑)已完成折叠
clearInterval(timer);
patchCollapseState();
return;
}
if (toggleViaFramework()) { // 否则由我们走原生入口折叠
clearInterval(timer);
patchCollapseState();
}
}, 150);
})();
// ---------------------------------------------------------------------
// 2) 触屏补位:mini 模式下子菜单是「悬浮浮层」,靠 mouseenter/mouseleave 开关,
// 而触屏 tap 只可靠触发 mouseenter、mouseleave 常常不来,浮层就赖着不走。
// 这里在点菜单以外的地方时主动收起浮层。
// ---------------------------------------------------------------------
function closeFlyout() {
var list = document.querySelectorAll("#side .layui-nav-child.layui-nav-hover");
for (var i = 0; i < list.length; i++) {
list[i].classList.remove("layui-nav-hover");
list[i].style.left = "0px";
list[i].style.top = "0px";
}
}
document.addEventListener("touchstart", function (e) {
if (!isMobile()) {
return;
}
var t = e.target;
if (t && t.closest && t.closest("#side")) {
return; // 点在菜单里,交给框架自己处理
}
closeFlyout();
}, { passive: true });
// ---------------------------------------------------------------------
// 3) 移动端习惯:展开态下点了叶子菜单(要跳转的项)之后自动收起侧栏,
// 否则 220px 的侧栏会一直压在内容上。目录项(有子菜单)不收起。
// 注:a.site-demo-active 只挂在可跳转的叶子菜单上,目录项没有这个 class。
// ---------------------------------------------------------------------
document.addEventListener("click", function (e) {
if (!isMobile()) {
return;
}
var t = e.target;
if (!t || !t.closest) {
return;
}
if (!t.closest('#side a.site-demo-active')) {
return;
}
if (isCollapsed()) {
return;
}
setTimeout(function () {
if (!isCollapsed()) {
toggleViaFramework();
}
}, 250);
});
})();