Files
ulthon_admin/public/static/plugs/lay-module/layuimini/miniMenu.js
2026-10-10 14:26:51 +00:00

529 lines
26 KiB
PHP
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.

(function () {
var element = layui.element,
laytpl = layui.laytpl,
layer = layui.layer;
var miniMenu = {
// 菜单数据缓存(renderMultiModule 入口保存,收藏过滤与面板渲染复用;保存原 menuList 引用)
menuData: [],
// 顶部「全部模块」面板 layer index(null=面板未打开)
modulePanelIndex: null,
/**
* 菜单初始化
* @param options.menuList 菜单数据信息
* @param options.menuChildOpen 是否展开子菜单
*/
render: function (options) {
options.menuList = options.menuList || [];
options.menuChildOpen = options.menuChildOpen || false;
if (!ua.checkMobile()) {
miniMenu.renderMultiModule(options.menuList, options.menuChildOpen);
} else {
miniMenu.renderSingleModule(options.menuList, options.menuChildOpen);
}
miniMenu.listen();
},
/**
* 单模块
* @param menuList 菜单数据
* @param menuChildOpen 是否默认展开
*/
renderSingleModule: function (menuList, menuChildOpen) {
menuList = menuList || [];
var leftMenuHtml = '',
childOpenClass = '',
leftMenuCheckDefault = 'layui-this';
var me = this;
if (menuChildOpen) childOpenClass = ' layui-nav-itemed';
leftMenuHtml = this.renderLeftMenu(menuList, { childOpenClass: childOpenClass });
$('.layui-layout-body').addClass('layuimini-single-module'); //单模块标识
$('.layuimini-menu-left').html(leftMenuHtml);
element.init();
},
/**
* 渲染一级菜单
*/
compileMenu: function (menu, isSub) {
var menuHtml = '<li {{#if( d.menu){ }} data-menu="{{-d.menu}}" {{#}}} class="layui-nav-item menu-li {{-d.childOpenClass}} {{-d.className}}" {{#if( d.id){ }} id="{{-d.id}}" {{#}}}> <a title="{{-d.title}}" {{#if( d.href){ }} layuimini-href="{{-d.href}}" {{#}}} {{#if( d.target){ }} target="{{-d.target}}" {{#}}} href="javascript:;">{{#if( d.icon){ }} <i class="{{-d.icon}}"></i> {{#}}} <span class="layui-left-nav">{{-d.title}}</span></a> {{# if(d.children){}} {{-d.children}} {{#}}} </li>';
if (isSub) {
menuHtml = '<dd class="menu-dd {{-d.childOpenClass}} {{- d.className }}"> <a href="javascript:;" title="{{-d.title}}" {{#if( d.menu){ }} data-menu="{{-d.menu}}" {{#}}} {{#if( d.id){ }} id="{{-d.id}}" {{#}}} {{#if(( !d.child || !d.child.length ) && d.href){ }} layuimini-href="{{-d.href}}" {{#}}} {{#if( d.target){ }} target="{{-d.target}}" {{#}}}> {{#if( d.icon){ }} <i class="{{-d.icon}}"></i> {{#}}} <span class="layui-left-nav"> {{-d.title}}</span></a> {{# if(d.children){}} {{-d.children}} {{#}}}</dd>';
}
return laytpl(menuHtml).render(menu);
},
compileMenuContainer: function (menu, isSub) {
var wrapperHtml = '<ul class="layui-nav layui-nav-tree layui-left-nav-tree {{-d.className}}" id="{{-d.id}}">{{-d.children}}</ul>';
if (isSub) {
wrapperHtml = '<dl class="layui-nav-child ">{{-d.children}}</dl>';
}
if (!menu.children) {
return "";
}
return laytpl(wrapperHtml).render(menu);
},
each: function (list, callback) {
var _list = [];
for (var i = 0, length = list.length; i < length; i++) {
_list[i] = callback(i, list[i]);
}
return _list;
},
renderChildrenMenu: function (menuList, options) {
var me = this;
menuList = menuList || [];
var html = this.each(menuList, function (idx, menu) {
if (menu.child && menu.child.length) {
menu.children = me.renderChildrenMenu(menu.child, { childOpenClass: options.childOpenClass || '' });
}
menu.className = "";
menu.childOpenClass = options.childOpenClass || '';
return me.compileMenu(menu, true);
}).join("");
return me.compileMenuContainer({ children: html }, true);
},
renderLeftMenu: function (leftMenus, options) {
options = options || {};
var me = this;
var leftMenusHtml = me.each(leftMenus || [], function (idx, leftMenu) { // 左侧菜单遍历
var children = me.renderChildrenMenu(leftMenu.child, { childOpenClass: options.childOpenClass });
var leftMenuHtml = me.compileMenu({
href: leftMenu.href,
target: leftMenu.target,
childOpenClass: options.childOpenClass,
icon: leftMenu.icon,
title: leftMenu.title,
children: children,
className: '',
});
return leftMenuHtml;
}).join("");
leftMenusHtml = me.compileMenuContainer({ id: options.parentMenuId, className: options.leftMenuCheckDefault, children: leftMenusHtml });
return leftMenusHtml;
},
/**
* 头部模块收藏存储适配层(以 title 为唯一标识,未来可整体替换为后端实现)
*/
getFavorites: function () {
var favorites = [];
try {
favorites = JSON.parse(localStorage.getItem('layuiminiHeaderModuleFavorites'));
} catch (e) {
favorites = [];
}
if (!Array.isArray(favorites)) {
return [];
}
// 与当前菜单 title 求交集,过滤已失效项,保持存储顺序
var valid = [];
miniMenu.each(miniMenu.menuData || [], function (index, menu) {
valid.push(menu.title);
});
var result = [];
miniMenu.each(favorites, function (index, title) {
if (valid.indexOf(title) !== -1) {
result.push(title);
}
});
return result;
},
/**
* 写回收藏列表
*/
setFavorites: function (list) {
localStorage.setItem('layuiminiHeaderModuleFavorites', JSON.stringify(list || []));
},
/**
* 是否已收藏
*/
isFavorite: function (title) {
return miniMenu.getFavorites().indexOf(title) !== -1;
},
/**
* 切换收藏状态,返回切换后的新状态(true=已收藏)
*/
toggleFavorite: function (title) {
var favorites = miniMenu.getFavorites();
var isAdd = favorites.indexOf(title) === -1;
if (isAdd) {
favorites.push(title);
} else {
favorites.splice(favorites.indexOf(title), 1);
}
miniMenu.setFavorites(favorites);
return isAdd;
},
/**
* 头部模块排序:收藏组在前(组内保持菜单原序),未收藏组回填在后(保持菜单原序)
* 返回新数组、不修改入参;multi_module_N 下标仍按原 menuList 索引分配,仅排序显示顺序
* 注:收藏以 title 为唯一标识,顶级模块重名视为同一收藏(同时置顶/同时取消)——
* 框架菜单管理实际不产生重名顶级模块,此为已文档化假设而非保证
*/
sortModules: function (menuList) {
var favorites = miniMenu.getFavorites();
var sorted = [];
var backfill = [];
miniMenu.each(menuList || [], function (index, menu) {
if (favorites.indexOf(menu.title) !== -1) {
sorted.push(menu);
} else {
backfill.push(menu);
}
});
return sorted.concat(backfill);
},
/**
* 多模块
* @param menuList 菜单数据
* @param menuChildOpen 是否默认展开
*/
renderMultiModule: function (menuList, menuChildOpen) {
menuList = menuList || [];
var me = this;
miniMenu.menuData = menuList; //入口保存,注意为原数组引用
var sortedMenuList = this.sortModules(menuList); //头部显示顺序:收藏置顶+原序回填,仅排序不改下标映射
var firstMenu = 'multi_module_' + menuList.indexOf(sortedMenuList[0]); //排序后首模块,首屏一致性锚点
var headerMenuHtml = '',
leftMenuHtml = '',
childOpenClass = '',
headerMenuCheckDefault = 'layui-this';
if (menuChildOpen) childOpenClass = ' layui-nav-itemed';
var headerMenuHtml = this.each(sortedMenuList, function (index, val) { //顶部菜单渲染(按排序后顺序编译,data-menu 仍按原 menuList 下标映射)
var menu = 'multi_module_' + menuList.indexOf(val);
var id = menu + "HeaderId";
var topMenuItemHtml = "";
topMenuItemHtml = me.compileMenu({
className: headerMenuCheckDefault,
menu: menu,
id: id,
title: val.title,
href: "",
target: "",
children: ""
});
headerMenuCheckDefault = "";
return topMenuItemHtml;
}).join("");
leftMenuHtml = this.each(menuList, function (index, val) { //左侧菜单累积与 ul#multi_module_N 分配保持原 menuList 顺序(收藏只影响头部与面板显示顺序)
var menu = 'multi_module_' + index;
var leftMenuCheckDefault = (menu === firstMenu) ? 'layui-this' : 'layui-hide'; //默认可见 ul=排序后首模块,与头部激活项首屏一致
return me.renderLeftMenu(val.child, {
parentMenuId: menu,
childOpenClass: childOpenClass,
leftMenuCheckDefault: leftMenuCheckDefault
});
}).join("");
$('.layui-layout-body').addClass('layuimini-multi-module'); //多模块标识
$('.layuimini-menu-header-pc').html(headerMenuHtml); //电脑
// 常驻「更多」li:PC 多模块路径专属;自身不带 data-menu(不参与折叠遍历),li 带 lay-unselect 防 clickThis 洗牌激活态;
// 不手写 span.layui-nav-more(2.10.1 中手写不可见且会抑制自动注入箭头),由下方 element.init() 自动注入 chevron icon
$('.layuimini-menu-header-pc').append('<li class="layui-nav-item layuimini-header-more" lay-unselect><a title="更多">更多</a><dl class="layui-nav-child"></dl></li>');
$('.layuimini-menu-left').html(leftMenuHtml);
element.init();
// 首次渲染后重算宽度折叠,并同步「更多」激活态
miniMenu.relayoutHeaderMenu();
miniMenu.syncHeaderActiveState();
},
/**
* 头部模块 DOM 重排:头部 li 显示顺序跟随 sortModules(收藏置顶+原序回填),「更多」li 常驻末尾
* 星标收藏切换后调用实现头部即时重排;移动 li 不改类名,激活态/折叠态由后续 relayout 重算
*/
reorderHeaderMenu: function () {
var $headerMenu = $('.layuimini-menu-header-pc');
var $more = $headerMenu.children('.layuimini-header-more');
if (!$headerMenu.length || !$more.length) {
return;
}
var sortedMenuList = miniMenu.sortModules(miniMenu.menuData);
var moreEl = $more[0];
miniMenu.each(sortedMenuList, function (index, menu) {
var li = document.getElementById('multi_module_' + miniMenu.menuData.indexOf(menu) + 'HeaderId');
if (li) {
$headerMenu[0].insertBefore(li, moreEl); //依次前插到「更多」li 之前,形成排序后顺序
}
});
},
/**
* 头部模块宽度自适应折叠重算:累计宽度超出可用宽度的模块收纳进「更多」下拉
*/
relayoutHeaderMenu: function () {
var $headerMenu = $('.layuimini-menu-header-pc');
if (!$headerMenu.is(':visible')) { //可见性守卫:≤1024 时 pc-show 隐藏,测量隐藏容器得到全零宽度脏状态
return;
}
var $more = $headerMenu.children('.layuimini-header-more');
if (!$more.length) {
return;
}
// 可用宽度 = 右侧功能区左边界 - 头部菜单容器左边界 - 安全边距 - 「更多」自身常驻占位
var availableWidth = $('.layui-layout-right').first().offset().left - $headerMenu.offset().left - 12 - $more.outerWidth(true);
var $items = $headerMenu.children('li[data-menu]'); //仅模块 li;「菜单」模板 li 无 data-menu 不会被触及
$items.removeClass('layui-hide'); //测量前先全部回填可见,保证变宽时能恢复
var usedWidth = 0,
hiddenMenus = [];
$items.each(function () {
var $item = $(this);
usedWidth += $item.outerWidth(true);
if (usedWidth > availableWidth) {
$item.addClass('layui-hide');
hiddenMenus.push($item.attr('data-menu'));
}
});
// 用被隐藏模块按显示顺序重建下拉:dd 带 lay-unselect 防 clickThis 洗牌、带 a 承接主题悬停/颜色样式、不带 id
var ddHtml = '';
miniMenu.each(hiddenMenus, function (index, menuId) {
var menuIndex = parseInt(menuId.replace('multi_module_', ''), 10);
var menu = (miniMenu.menuData || [])[menuIndex] || {};
var icon = menu.icon ? menu.icon : '';
ddHtml += '<dd lay-unselect data-menu="' + menuId + '"><a href="javascript:;"><i class="' + icon + '"></i> ' + (menu.title || '') + '</a></dd>';
});
$more.children('dl.layui-nav-child').html(ddHtml);
element.init(); //已核实安全:只重绑事件与 bar span,不重建 dd DOM,激活态得以保留
miniMenu.syncHeaderActiveState(); //折叠可能隐藏当前激活项,重算后立即同步
},
/**
* 「更多」与下拉 dd 的激活态同步:激活态真源是头部模块 li(即使被折叠隐藏)
*/
syncHeaderActiveState: function () {
var $more = $('.layuimini-header-more');
if (!$more.length) {
return;
}
var $activeLi = $('.layuimini-header-menu li.layui-this').not('.layuimini-header-more').first();
var isActiveHidden = $activeLi.length > 0 && $activeLi.hasClass('layui-hide');
if (isActiveHidden) {
$more.addClass('layui-this');
} else {
$more.removeClass('layui-this');
}
// 下拉内对应 dd 与隐藏 li 的激活态对齐
var $dd = $more.find('dl.layui-nav-child dd');
$dd.removeClass('layui-this');
if (isActiveHidden) {
$dd.filter('[data-menu="' + $activeLi.attr('data-menu') + '"]').addClass('layui-this');
}
},
/**
* 顶部「全部模块」面板打开:menuData 经 sortModules 平铺为 tile(收藏置顶+回填序)
*/
openModulePanel: function () {
var me = this;
var favorites = me.getFavorites();
var sortedMenuList = me.sortModules(me.menuData);
var tileHtml = '';
me.each(sortedMenuList, function (index, menu) {
// data-header-id 按 menuList 原下标映射(排序只影响显示顺序,与头部 li id 分配规则一致);不带 data-menu,避免与 [data-menu] 委托双触发
var originalIndex = me.menuData.indexOf(menu);
var starActive = favorites.indexOf(menu.title) !== -1 ? ' layuimini-star-active' : '';
var icon = menu.icon ? menu.icon : '';
tileHtml += '<div class="layuimini-module-panel-item" data-header-id="multi_module_' + originalIndex + 'HeaderId" data-title="' + menu.title + '">' +
'<i class="' + icon + '"></i>' +
'<div>' + menu.title + '</div>' +
'<span class="layuimini-star fa fa-star-o' + starActive + '"></span>' +
'</div>';
});
// 配色继承:非透明头部主题时同步头部背景/字色到面板根容器;透明主题(rgba(0,0,0,0))保留 CSS 白底深字兜底
var panelStyle = '';
var headerEl = $('.layui-header').first()[0];
if (headerEl) {
var headerStyle = getComputedStyle(headerEl);
if (headerStyle.backgroundColor !== 'rgba(0, 0, 0, 0)') {
panelStyle = ' style="background-color:' + headerStyle.backgroundColor + ';color:' + headerStyle.color + ';"';
}
}
// 打开期间收起「更多」hover 下拉,避免遮罩后方残留展开外观(关闭时移除)
$('.layuimini-header-more').addClass('layuimini-header-more-panelopen');
me.modulePanelIndex = layer.open({
type: 1,
title: '全部模块',
anim: 2,
offset: 't',
area: ['100%', 'auto'],
shade: 0.2,
shadeClose: true,
closeBtn: 0,
content: '<div class="layuimini-module-panel"' + panelStyle + '><div class="layuimini-module-panel-grid">' + tileHtml + '</div></div>',
end: function () {
me.closeModulePanel(); //遮罩/切tile等 layer 原生关闭路径统一走清理
}
});
},
/**
* 顶部面板关闭:幂等(end 回调重入时 index 已置 null,重复调用安全)
*/
closeModulePanel: function () {
var index = miniMenu.modulePanelIndex;
miniMenu.modulePanelIndex = null;
if (index) {
layer.close(index);
}
$('.layuimini-header-more').removeClass('layuimini-header-more-panelopen');
},
/**
* 轻量防抖:delay 内重复触发只执行最后一次
*/
debounce: function (fn, delay) {
var timer = null;
return function () {
var context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
},
/**
* 监听
*/
listen: function () {
/**
* 菜单模块切换
*/
$('body').on('click', '[data-menu]', function () {
var loading = layer.load(0, { shade: false, time: 2 * 1000 });
var menuId = $(this).attr('data-menu');
var $headerLi = $("#" + menuId + "HeaderId");
// header:清空范围含被折叠隐藏的 li;激活态真源始终是头部模块 li(即使隐藏),不依赖 $(this) 高亮
$(".layuimini-header-menu .layui-nav-item").removeClass('layui-this');
$headerLi.addClass('layui-this');
// 被点击元素不是头部模块 li 本身(即「更多」下拉 dd 等克隆载体)时,才给它视觉高亮
if (!$(this).is($headerLi)) {
$(this).addClass('layui-this');
}
// left
$(".layuimini-menu-left .layui-nav.layui-nav-tree.layui-this").addClass('layui-hide');
$(".layuimini-menu-left .layui-nav.layui-nav-tree.layui-this.layui-hide").removeClass('layui-this');
$("#" + menuId).removeClass('layui-hide');
$("#" + menuId).addClass('layui-this');
miniMenu.syncHeaderActiveState();
layer.close(loading);
});
/**
* 窗口尺寸变化时重算头部折叠(防抖,仅绑定一次,避免重渲染重复绑定)
*/
if (!miniMenu._resizeListenerBound) {
miniMenu._resizeListenerBound = true;
$(window).on('resize', miniMenu.debounce(function () {
miniMenu.relayoutHeaderMenu();
}, 150));
}
/**
* 菜单缩放
*/
$('body').on('click', '.layuimini-site-mobile', function () {
var loading = layer.load(0, { shade: false, time: 2 * 1000 });
var isShow = $('.layuimini-tool [data-side-fold]').attr('data-side-fold');
if (isShow == 1) { // 缩放
$('.layuimini-tool [data-side-fold]').attr('data-side-fold', 0);
$('.layuimini-tool [data-side-fold]').attr('class', 'fa fa-indent');
$('.layui-layout-body').removeClass('layuimini-all');
$('.layui-layout-body').addClass('layuimini-mini');
} else { // 正常
$('.layuimini-tool [data-side-fold]').attr('data-side-fold', 1);
$('.layuimini-tool [data-side-fold]').attr('class', 'fa fa-outdent');
$('.layui-layout-body').removeClass('layuimini-mini');
$('.layui-layout-body').addClass('layuimini-all');
layer.close(window.openTips);
}
element.init();
setTimeout(miniMenu.relayoutHeaderMenu, 260); //左侧布局带 .2s 过渡,同步测量会读到中间值,等过渡结束再重算
layer.close(loading);
});
/**
* 菜单缩放
*/
$('body').on('click', '[data-side-fold]', function () {
var loading = layer.load(0, { shade: false, time: 2 * 1000 });
var isShow = $('.layuimini-tool [data-side-fold]').attr('data-side-fold');
if (isShow == 1) { // 缩放
$('.layuimini-tool [data-side-fold]').attr('data-side-fold', 0);
$('.layuimini-tool [data-side-fold]').attr('class', 'fa fa-indent');
$('.layui-layout-body').removeClass('layuimini-all');
$('.layui-layout-body').addClass('layuimini-mini');
// $(".menu-li").each(function (idx,el) {
// $(el).addClass("hidden-sub-menu");
// });
} else { // 正常
$('.layuimini-tool [data-side-fold]').attr('data-side-fold', 1);
$('.layuimini-tool [data-side-fold]').attr('class', 'fa fa-outdent');
$('.layui-layout-body').removeClass('layuimini-mini');
$('.layui-layout-body').addClass('layuimini-all');
// $(".menu-li").each(function (idx,el) {
// $(el).removeClass("hidden-sub-menu");
// });
layer.close(window.openTips);
}
element.init();
setTimeout(miniMenu.relayoutHeaderMenu, 260); //左侧布局带 .2s 过渡,同步测量会读到中间值,等过渡结束再重算
layer.close(loading);
});
/**
* 顶部「全部模块」面板:委托绑定一次即可(tile/星标均为动态 DOM,重渲染无需重绑)
*/
if (!miniMenu._panelListenerBound) {
miniMenu._panelListenerBound = true;
// 打开/切换面板:「更多」li 自身不带 data-menu,stopPropagation 隔离其他 body 委托
$('body').on('click', '.layuimini-header-more > a', function (e) {
e.stopPropagation();
if (miniMenu.modulePanelIndex) {
miniMenu.closeModulePanel();
} else {
miniMenu.openModulePanel();
}
});
// ESC 关闭面板(layui layer 原生不支持 ESC,自实现;仅在面板存在时响应,不影响其他弹层)
$(document).on('keydown', function (e) {
if (e.keyCode === 27 && miniMenu.modulePanelIndex) {
miniMenu.closeModulePanel();
}
});
// tile 主体点击:触发真实头部 li(可见或折叠隐藏均可),走 [data-menu] 委托完整链路后关闭面板
$('body').on('click', '.layuimini-module-panel-item', function () {
$('#' + $(this).attr('data-header-id')).trigger('click');
layer.close(miniMenu.modulePanelIndex);
});
// 星标点击:切换收藏 → 头部即时重排(重排序+折叠重算)→ 就地更新星标类名(面板保持打开,支持连续操作)
$('body').on('click', '.layuimini-module-panel-item .layuimini-star', function (e) {
e.stopPropagation(); //阻断冒泡,避免同时触发 tile 的切模块点击
var isAdd = miniMenu.toggleFavorite($(this).closest('.layuimini-module-panel-item').attr('data-title'));
miniMenu.reorderHeaderMenu();
miniMenu.relayoutHeaderMenu();
$(this).toggleClass('layuimini-star-active', isAdd);
});
}
},
};
window.miniMenu = miniMenu;
})();