Files
superlink/static/js/common.js
T

454 lines
17 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.
/**
* 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', '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 += '<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
/** 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 += '<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();
}