448 lines
17 KiB
JavaScript
448 lines
17 KiB
JavaScript
/**
|
||
* common.js - 通用函数(httpGet/httpPost/弹窗/分页/日期格式化/菜单渲染/侧边栏/用户菜单)
|
||
*/
|
||
|
||
/* ============ HTTP 封装 ============ */
|
||
|
||
/**
|
||
* GET 请求(返回 Promise<data>)
|
||
*/
|
||
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<data>)
|
||
*/
|
||
function httpPost(url, data, isFormData) {
|
||
var opts = {
|
||
url: BASE_URL + url,
|
||
method: 'POST',
|
||
dataType: 'json',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||
};
|
||
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 += '<div style="font-size:14px;color:#e74c3c;margin-bottom:4px;">' + escHtml(x.label) + '「' + escHtml(x.value) + '」出现重复</div>' +
|
||
'<div style="font-size:13px;color:#5a6472;margin-bottom:10px;">重复数据:' + escHtml(x.table) + ' 表,id=' + x.id + '</div>';
|
||
});
|
||
layer.open({
|
||
type: 1,
|
||
title: '数据重复提示',
|
||
area: ['440px', 'auto'],
|
||
content: '<div style="padding:20px 24px;">' + items + '</div>',
|
||
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, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
/** 手机号清洗:清除所有空白(含数字中间的空格),如 "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;
|
||
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 += '<div class="menu-group' + (isOpen ? ' open' : '') + '">' +
|
||
'<div class="menu-group-title" onclick="toggleMenuGroup(this)">' +
|
||
'<span class="icon">' + (ICONS[g.icon] || '') + '</span>' +
|
||
'<span class="label">' + g.name + '</span>' +
|
||
'<span class="arrow">▸</span></div>' +
|
||
'<div class="menu-group-body"' + (isOpen ? ' style="display:block;"' : '') + '>';
|
||
visibleChildren.forEach(function (c) {
|
||
var isActive = (c.url === currentPage);
|
||
html += '<a class="menu-item sub' + (isActive ? ' active' : '') + '" href="' + c.url + '">' +
|
||
'<span class="label">' + c.name + '</span></a>';
|
||
});
|
||
html += '</div></div>';
|
||
} else {
|
||
if (permissions.indexOf(g.key) < 0) return;
|
||
var isActive = (g.key === activeKey || g.url === currentPage);
|
||
html += '<a class="menu-item' + (isActive ? ' active' : '') + '" href="' + g.url + '">' +
|
||
'<span class="icon">' + (ICONS[g.icon] || '') + '</span>' +
|
||
'<span class="label">' + g.name + '</span></a>';
|
||
}
|
||
});
|
||
|
||
$('#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 =
|
||
'<div class="layout">' +
|
||
' <aside class="sidebar">' +
|
||
' <a class="logo" href="dashboard.html" title="返回综合概览">Super<span>Link</span><span class="version">' + (window.APP_VERSION || 'v1.0.00') + '</span></a>' +
|
||
' <nav class="menu" id="sidebar-menu"></nav>' +
|
||
' </aside>' +
|
||
' <div class="main">' +
|
||
' <div class="topbar">' +
|
||
' <span class="sidebar-toggle" id="sidebar-toggle" onclick="toggleSidebar()" title="折叠/展开菜单">' + (ICONS.menu || '') + '</span>' +
|
||
' <div class="page-title">' + pageTitle + '</div>' +
|
||
(breadcrumb ? '<div class="breadcrumb">' + breadcrumb + '</div>' : '') +
|
||
' <div class="topbar-spacer"></div>' +
|
||
' <div class="user-menu" id="user-menu">' +
|
||
' <span class="user-menu-trigger">' +
|
||
' <span class="avatar" id="um-avatar">U</span>' +
|
||
' <span id="um-name">加载中...</span>' +
|
||
' <span class="caret">▾</span>' +
|
||
' </span>' +
|
||
' <div class="user-menu-dropdown">' +
|
||
' <div class="um-head">' +
|
||
' <div class="um-name" id="um-detail-name"></div>' +
|
||
' <div class="um-sub" id="um-detail-account"></div>' +
|
||
' </div>' +
|
||
' <div class="um-item"><span class="um-label">角色</span><b id="um-role">-</b></div>' +
|
||
' <div class="um-item"><span class="um-label">登录状态</span><b id="um-status">-</b></div>' +
|
||
' <div class="um-divider"></div>' +
|
||
' <a class="um-action" id="um-login" style="display:none;">登 录</a>' +
|
||
' <a class="um-action um-danger" id="um-logout" style="display:none;">退出登录</a>' +
|
||
' </div>' +
|
||
' </div>' +
|
||
' </div>' +
|
||
' <div class="content" id="page-content"></div>' +
|
||
' <footer class="footer">' + (window.FOOTER_TEXT || '© SuperLink 管理系统') + '</footer>' +
|
||
' </div>' +
|
||
'</div>';
|
||
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 = '<span>共 ' + total + ' 条 / ' + pages + ' 页</span>';
|
||
html += '<span class="pg' + (page <= 1 ? ' disabled' : '') + '" data-p="' + (page - 1) + '">上一页</span>';
|
||
|
||
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 += '<span class="pg' + (i === page ? ' current' : '') + '" data-p="' + i + '">' + i + '</span>';
|
||
}
|
||
|
||
html += '<span class="pg' + (page >= pages ? ' disabled' : '') + '" data-p="' + (page + 1) + '">下一页</span>';
|
||
$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();
|
||
}
|