根因:此前为移动端手写的一段折叠逻辑直接给 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 一行配置。
149 lines
5.8 KiB
JavaScript
149 lines
5.8 KiB
JavaScript
/**
|
||
* 移动端侧栏增强(视口 ≤ 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 覆写为 undefined(falsy)。
|
||
* 折叠后若 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);
|
||
});
|
||
})();
|