/** * common.js - 通用函数(httpGet/httpPost/弹窗/分页/日期格式化/菜单渲染/侧边栏/用户菜单) */ /* ============ HTTP 封装 ============ */ /** * GET 请求(返回 Promise) */ function httpGet(url, params) { var full = BASE_URL + url; if (params) { var qs = $.param(params); full += (full.indexOf('?') >= 0 ? '&' : '?') + qs; } return $.ajax({ url: full, method: 'GET', dataType: 'json' }).then(handleResp, handleError); } /** * POST 请求(写类操作自动携带 X-Requested-With 头,返回 Promise) */ function httpPost(url, data, isFormData) { var opts = { url: BASE_URL + url, method: 'POST', dataType: 'json', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': window.CSRF_TOKEN || '' } }; if (isFormData) { opts.data = data; opts.processData = false; opts.contentType = false; } else { opts.data = data; } return $.ajax(opts).then(handleResp, handleError); } /** 统一处理响应:code!=0 弹错并 reject;401 立即回登录页 */ function handleResp(resp) { if (resp && resp.code === 0) { return resp.data; } if (resp && resp.code === 401) { location.href = 'index.html'; return $.Deferred().reject(resp).promise(); } // 重复数据:单弹窗列出所有重复字段 + 重复记录的表名与 id if (resp && resp.code === 1001 && resp.data) { var list = $.isArray(resp.data) ? resp.data : [resp.data]; var items = ''; (list || []).forEach(function (x) { items += '
' + escHtml(x.label) + '「' + escHtml(x.value) + '」出现重复
' + '
重复数据:' + escHtml(x.table) + ' 表,id=' + x.id + '
'; }); layer.open({ type: 1, title: '数据重复提示', area: ['440px', 'auto'], content: '
' + items + '
', btn: ['知道了'] }); return $.Deferred().reject(resp).promise(); } var msg = (resp && resp.msg) ? resp.msg : '请求失败'; layer.msg(msg, { icon: 2 }); return $.Deferred().reject(resp).promise(); } function handleError(xhr) { if (xhr.status === 401) { location.href = 'index.html'; return $.Deferred().reject(xhr).promise(); } var msg = '网络错误'; try { var resp = xhr.responseJSON; if (resp && resp.msg) msg = resp.msg; } catch (e) {} layer.msg(msg, { icon: 2 }); return $.Deferred().reject(xhr).promise(); } /* ============ 弹窗封装(基于 layer) ============ */ var Dialog = { alert: function (msg, cb) { layer.alert(msg, { icon: 0 }, cb || function () {}); }, success: function (msg, cb) { layer.msg(msg, { icon: 1, time: 1200 }, cb || function () {}); }, error: function (msg) { layer.msg(msg, { icon: 2 }); }, confirm: function (msg, cb) { layer.confirm(msg, { icon: 3, title: '确认操作' }, function (idx) { layer.close(idx); cb && cb(); }); }, open: function (opts) { return layer.open({ type: 1, title: opts.title || '操作', area: opts.area || ['720px', 'auto'], content: opts.content, btn: opts.btn !== false ? ['确定', '取消'] : null, yes: opts.yes || null, end: opts.end || null }); }, close: function (idx) { layer.close(idx); } }; /* ============ 日期格式化 ============ */ function fmtDate(str, withTime) { if (!str) return '-'; var d = new Date(str.replace(/-/g, '/')); if (isNaN(d.getTime())) return str; var p = function (n) { return n < 10 ? '0' + n : '' + n; }; var s = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()); if (withTime) s += ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); return s; } function escHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } /** esc 别名:与后端 esc() 同名,前端渲染动态内容统一转义 */ function esc(str) { return escHtml(str); } /** 手机号清洗:清除所有空白(含数字中间的空格),如 "138 1234 5678" -> "13812345678" */ function cleanPhone(v) { return String(v == null ? '' : v).replace(/\s+/g, ''); } /** 邮箱清洗:清除首尾空格 */ function cleanEmail(v) { return String(v == null ? '' : v).trim(); } /** 格式校验:邮箱 / 国内手机号 / 身份证 */ function validateEmail(v) { return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(v || '').trim()); } function validatePhoneCN(v) { return /^1[3-9]\d{9}$/.test(String(v || '').trim()); } function validateIdCard(v) { return /^\d{15}$|^\d{17}[\dXx]$/.test(String(v || '').trim()); } /* ============ 会话与权限 ============ */ /** * 校验当前页面权限,无权限则提示并跳回首页 * @param {string} key 菜单标识 * @returns {boolean} 是否有权限 */ function checkPagePermission(key) { var perms = (window.CURRENT_USER && window.CURRENT_USER.permissions) || []; if (perms.indexOf(key) < 0) { Dialog.alert('您没有访问该模块的权限', function () { location.href = 'dashboard.html'; }); return false; } return true; } /** * 页面初始化:校验登录态 + 渲染左侧菜单 + 填充右上角用户信息 * 注意:成功回调内包裹 try/catch——渲染异常只记日志,不触发登录页跳转; * .fail 仅在会话真实失效(401)时才跳转登录页。 */ function initPage(activeKey, cb) { httpGet('auth/session.php').then(function (user) { window.CURRENT_USER = user; window.CSRF_TOKEN = user.csrf_token || ''; // 写接口统一携带 X-CSRF-Token try { renderMenu(activeKey, user.permissions || []); // 右上角用户菜单 var name = user.real_name || user.username; $('#um-name').text(name); $('#um-detail-name').text(name); $('#um-detail-account').text('@' + user.username); $('#um-avatar').text(name.charAt(0).toUpperCase()); $('#um-role').text(user.role_name || '-'); $('#um-status').text('已登录').removeClass('um-off').addClass('um-ok'); $('#um-logout').show(); $('#um-login').hide(); cb && cb(user); } catch (e) { console.error('initPage render error:', e); } }).fail(function () { // 会话失效/未登录:立即回登录页,不允许停留在当前页面继续操作 try { var a = arguments[0]; var desc; if (a && a.responseText) desc = 'HTTP/' + a.status + ' respText=' + a.responseText.substring(0, 300); else if (a instanceof Error) desc = 'Error: ' + a.message; else if (a && a.readyState !== undefined) desc = 'jqXHR status=' + a.status + ' statusText=' + a.statusText; else desc = 'arg0=' + String(a); console.error('initPage session check failed:', desc); } catch (e) { console.error('initPage fail (unserializable)', String(e)); } location.href = 'index.html'; }); } /* ============ 侧边栏渲染 ============ */ /** * 渲染左侧导航菜单(分级,扁平图标;二级菜单不带图标) * - 分组:拥有任意子项权限即显示;子项逐个按权限过滤 * - 高亮:子项按“当前页面文件名”匹配(避免营销服务共用权限时全部选中);一级项按 key/url */ function renderMenu(activeKey, permissions) { var currentPage = location.pathname.split('/').pop(); var html = ''; MENU_GROUPS.forEach(function (g) { if (g.children) { // 过滤出当前用户有权限的子项 var visibleChildren = g.children.filter(function (c) { return permissions.indexOf(c.key) >= 0; }); if (!visibleChildren.length) return; var isOpen = visibleChildren.some(function (c) { return c.url === currentPage; }); html += ''; } else { if (permissions.indexOf(g.key) < 0) return; var isActive = (g.key === activeKey || g.url === currentPage); html += '' + '' + (ICONS[g.icon] || '') + '' + '' + g.name + ''; } }); $('#sidebar-menu').html(html); } /** 展开/收起分组菜单(折叠态点击分组时先展开侧边栏) */ window.toggleMenuGroup = function (el) { var $layout = $('.layout'); if ($layout.hasClass('sidebar-collapsed')) { $layout.removeClass('sidebar-collapsed'); if (!window.matchMedia('(max-width: 768px)').matches) { localStorage.setItem('sl_sidebar_collapsed', '0'); } } var $group = $(el).closest('.menu-group'); var $body = $group.find('.menu-group-body'); var isOpen = $group.hasClass('open'); if (isOpen) { $group.removeClass('open'); $body.slideUp(150); } else { $group.addClass('open'); $body.slideDown(150); } }; /** 手动折叠/展开侧边栏 */ window.toggleSidebar = function () { var $layout = $('.layout'); $layout.toggleClass('sidebar-collapsed'); var collapsed = $layout.hasClass('sidebar-collapsed'); if (!window.matchMedia('(max-width: 768px)').matches) { localStorage.setItem('sl_sidebar_collapsed', collapsed ? '1' : '0'); } }; /** 初始化侧边栏状态:桌面读用户偏好;移动端(<768px)自动折叠 */ function initSidebar() { var mq = window.matchMedia('(max-width: 768px)'); function apply() { var $layout = $('.layout'); if (mq.matches) { $layout.addClass('sidebar-collapsed'); } else if (localStorage.getItem('sl_sidebar_collapsed') === '1') { $layout.addClass('sidebar-collapsed'); } else { $layout.removeClass('sidebar-collapsed'); } } apply(); if (mq.addEventListener) mq.addEventListener('change', apply); else if (mq.addListener) mq.addListener(apply); } /* ============ 页面通用骨架(侧边栏+顶栏+右上角用户菜单+页脚) ============ */ function renderShell(pageTitle, breadcrumb) { var html = '
' + ' ' + '
' + '
' + ' ' + (ICONS.menu || '') + '' + '
' + pageTitle + '
' + (breadcrumb ? '' : '') + '
' + '
' + ' ' + ' U' + ' 加载中...' + ' ▾' + ' ' + '
' + '
' + '
' + '
' + '
' + '
角色-
' + '
登录状态-
' + '
' + ' ' + ' ' + '
' + '
' + '
' + '
' + '
' + (window.FOOTER_TEXT || '© SuperLink 管理系统') + '
' + '
' + '
'; document.body.innerHTML = html; initSidebar(); // 右上角用户菜单:悬停显示(延迟关闭,保证能移入下拉点击)+ 点击切换(触屏) var menuTimer = null; $('#user-menu').on('mouseenter', function () { clearTimeout(menuTimer); $(this).addClass('open'); }); $('#user-menu').on('mouseleave', function () { var self = this; clearTimeout(menuTimer); menuTimer = setTimeout(function () { $(self).removeClass('open'); }, 250); }); $('#user-menu .user-menu-trigger').on('click', function (e) { e.stopPropagation(); $('#user-menu').toggleClass('open'); }); $(document).on('click', function () { $('#user-menu').removeClass('open'); }); $('#um-logout').on('click', function () { Dialog.confirm('确定要退出登录吗?', function () { httpPost('auth/logout.php').then(function () { location.href = 'index.html'; }); }); }); $('#um-login').on('click', function () { location.href = 'index.html'; }); } /* ============ 分页组件 ============ */ /** * 渲染分页条 * @param {object} pager { total, page, limit } * @param {function} onPage 翻页回调(page) */ function renderPagination($el, pager, onPage) { var total = pager.total || 0; var page = pager.page || 1; var limit = pager.limit || PAGE_SIZE; var pages = Math.max(1, Math.ceil(total / limit)); var html = '共 ' + total + ' 条 / ' + pages + ' 页'; html += '上一页'; var start = Math.max(1, page - 2); var end = Math.min(pages, start + 4); start = Math.max(1, end - 4); for (var i = start; i <= end; i++) { html += '' + i + ''; } html += '下一页'; $el.html(html); $el.find('span.pg').each(function () { var $this = $(this); if ($this.hasClass('disabled')) return; $this.on('click', function () { onPage && onPage(parseInt($this.data('p'), 10)); }); }); } /* ============ 文件下载(导出) ============ */ function download(url, params) { var full = BASE_URL + url; if (params) { full += (full.indexOf('?') >= 0 ? '&' : '?') + $.param(params); } location.href = full; } /* ============ 通用表格渲染辅助 ============ */ function buildFilterParam() { var obj = {}; $('.toolbar [data-filter]').each(function () { var $this = $(this); var key = $this.data('filter'); var val = $this.val(); if (val !== '' && val !== null && val !== undefined) { obj[key] = val; } }); return obj; } /* ============ 文件上传输入 ============ */ function triggerFileInput(accept, cb) { var input = document.createElement('input'); input.type = 'file'; input.accept = accept || '.csv'; input.style.display = 'none'; input.onchange = function () { if (input.files.length > 0) { cb(input.files[0]); } document.body.removeChild(input); }; document.body.appendChild(input); input.click(); }