//依赖类库:lodash.js + jquery + jquery.validate + bootstrap.emodal.js // 统一调试日志工具:仅在 otcdebug 开启时输出,默认静默(生产零噪音)。 // 开启:URL 加 ?otcdebug=1(会自动写入 localStorage,同源 iframe/弹窗自动继承); // 或控制台 localStorage.setItem('otcdebug','1')。关闭:localStorage.removeItem('otcdebug')。 // 业务脚本用 otcDebug.banner / otcDebug.log 替代裸 console.log(见 swapTradeEdit.js / fastVue.base.js)。 // // 命名约定(避免 __私有对象 + otcDebug.API 大小写混搭): // __diag —— 版本元数据对象(仅 _MainLayout 灌入 ylotc.__diag 时存在,仅供 banner 展示,不参与开关判定) // __debug —— 调试开关布尔值(唯一真相来源:localStorage 优先 → URL 参数 → 兜底 __diag.debug) var __diag = (window.ylotc && window.ylotc.__diag) || {}; var __debug = (function () { try { // 1) URL 参数优先(排查最直观);命中即写入 localStorage,供同源 iframe/弹窗继承 if (/[?&]otcdebug=1/.test(location.search)) { localStorage.setItem('otcdebug', '1'); return true; } // 2) 已持久化的开关(父页面开过一次,_InfoLayout 弹窗自动生效) if (localStorage.getItem('otcdebug') === '1') return true; } catch (e) { /* localStorage 不可用时忽略,继续走兜底 */ } // 3) 兜底:layout 灌入的 ylotc.__diag.debug(仅 _MainLayout 有) return !!__diag.debug; })(); window.otcDebug = { // 模块加载横幅:F12 一眼看到模块名+版本,用于排查"是不是加载了旧代码/旧缓存" banner: function (name, ver) { if (!__debug || !window.console) return; var git = (__diag.git || '').slice(0, 7); console.log('%c[' + name + '] v' + ver + ' (bundle=' + __diag.jsVersion + ', git=' + git + ', built=' + __diag.built + ')', 'color:#06c;font-weight:bold'); }, // 普通调试日志:透传参数,仅 debug 开启时输出 log: function () { if (!__debug || !window.console) return; console.log.apply(console, arguments); } }; var main = window.main || { version: "1.0" }; main.extend = $.extend; //数字格式化 /** * 构建数字格式化函数(依赖lodash.js) * @see Intl.NumberFormat * @param {any} precision - 小数位精度,默认2,如果是对象类,则当做带precision属性的options处理 * @param {object} options - 格式化选项 * @param {boolean} options.rounded - 是否四舍五入,明确传入false时禁止四舍五入 * @param {boolean} options.grouping - 是否使用千位分隔符,默认false * @param {boolean} options.percent - 是否百分比格式,默认false * @param {boolean} options.trimTailZeros - 是否去除小数点后的尾部的0,默认false * @param {any} options.prefmt * - 预先格式化,支持多种形式处理 * - 如果传入数组类型的参数值,则循环执行数组中的函数,如果执行结果为string或number类型,则prefmt值取此结果并跳出循环 * - 如果传入函数类型的参数值,prefmt值取函数执行结果 * - 如果prefmt值为数字类型,则格式化prefmt值 * - 如果prefmt值不为undefined|null|false,则返回prefmt值,否则忽略prefmt * @param {string} options.nanfmt - 输入值解析成数字为NAN时的格式化处理,类型为字符串,默认返回0的格式化数值 * @return {function} - 返回带fromat函数的格式化对象 */ main.numberFormat = function (precision, options) { if (typeof precision === 'object') { options = precision; precision = options.precision; } else if (!options || typeof options !== 'object') { options = {}; } //解析小数位精度 precision = parseInt(precision); if (isNaN(precision)) { precision = 2; } else if (precision < 0) { precision = 0; } else if (precision > 10) { precision = 10; } //构建Intl.NumberFormat const fmt = new Intl.NumberFormat('en', { useGrouping: !!options.grouping, minimumFractionDigits: precision, maximumFractionDigits: precision, style: options.percent ? 'percent' : 'decimal' }); //百分比格式特殊精度位处理 if (options.percent) precision += 2; //预格式化处理 function preformat(prefmt, fmtArgs) { //如果是数组类型,则以管道形式执行函数 if (Array.isArray(prefmt)) { var result = false; _.each(prefmt, x => { let match = typeof x === 'function'; while (match) { x = x.apply(this, fmtArgs); switch (typeof x) { case 'function': continue; case 'number': case 'string': result = x; return false; default: return true; } } }); return result; } //执行函数处理 while (typeof prefmt === 'function') { prefmt = prefmt.apply(this, fmtArgs); } return prefmt; } function _format(val) { //预格式化处理 let prefmt = preformat(options.prefmt, arguments); let type = typeof prefmt; if (type === 'number') { val = prefmt; } else if (type !== 'undefined' && prefmt !== null && prefmt !== false) { return prefmt; } if (precision <= 4) { val = _.round(val, precision + 4); } //数值格式化 if (options.rounded !== false) { let negative = val < 0; val = _.round(negative ? -val : val, precision); negative && (val = -val); } else { val = _.floor(val, precision); } if (isNaN(val)) { return typeof options.nanfmt === 'string' ? options.nanfmt : fmt.format(0); } return fmt.format(Math.abs(val) < 1e-6 ? 0 : val); } function format(val) { let str = _format(val); return options.trimTailZeros && str.endsWith("0") && precision > 0 ? str.replace(/\.?0+$/, '') : str; } format.precision = precision; return format; }; /** * 格式化数字(依赖lodash.js) * @param {object} number - 数字数值 * @param {any} precision - 小数位精度,默认2,如果是对象类,则当做带precision属性的options处理 * @param {object} options - 格式化选项 * @param {boolean} options.rounded - 是否四舍五入,类型为布尔值,默认true * @param {boolean} options.grouping - 是否使用千位分隔符,类型为布尔值,默认false * @param {boolean} options.percent - 是否百分比格式,类型为布尔值,默认false * @param {string} options.nanfmt - 输入值解析成数字为NAN时的格式化处理,类型为字符串,默认返回0的格式化数值 * @param {any} options.prefmt - 参考main.numberFormat * @return {function} - 返回带fromat函数的格式化对象 */ main.formatNumber = function (number, precision, options) { return new main.numberFormat(precision, options)(number); }; //各种常用处理 const __alert = function (message) { window.alert(message); return false; }; /** * 显示页面加载组件,等待操作执行完成 * @param {string} method -- 'show'||'hide' * @returns {jQuery} -- jQuery对象 */ $.fn.waitMe = function (method) { return this.each(function () { if (this.showCount === undefined || this.showCount === null) { this.showCount = 0; } var isBody = $(this).is('body'); var $waitme = $(this).children(".yl-waitme"); if (method === "show") { if (!$waitme.length) { !isBody && $(this).css("position") === "static" && $(this).css("position", "relative"); $waitme = $('
'); $waitme.appendTo($(this)); } $waitme.show(); this.showCount++; } else if (this.showCount > 0) { (this.showCount -= 1) === 0 && $waitme.remove(); } }); }; main.logError = function (data) { if (!data) return; if (console && typeof console.error === "function") { console.error(data); } else if ($.isPlainObject(data) || data.length) { alert(JSON.stringify(data)); } else { alert(data); } }; const _systemmsg = []; /** * 显示通知消息 * @param {any} message -- 消息文本 * @param {any} time -- 显示时间 * @returns {boolean} -- false */ main.message = function (message, time) { main.toast(message, time); try { if (_systemmsg.length >= 3) { _systemmsg.splice(0, 1); } _systemmsg.push(message); $("#systemspanmsg").html(_systemmsg.join(";")); } catch (e) { console.log("[ERROR:main.message]" + e); } return false; }; main.toast = function (message, delay) { if (!$('#toast-container').length) { $('
').appendTo('body'); } var $t = $(`
提示
${message}
`); $t.appendTo('#toast-container').toast({ autohide: true, delay: delay || 3000 }).toast('show') .on('hidden.bs.toast', function () { $t.remove(); }).on('click', function () { $(this).toast('hide'); }); return false; }; /** * 显示警告信息 * @param {any} message -- 消息文本 * @returns {boolean} -- false */ main.warn = function (message) { return main.message(message, 60 * 60 * 1000); }; /** * 显示警告弹窗 * @param {any} message -- 消息文本 * @returns {boolean} -- false */ main.alert = function (message, okCall) { $.alert({ title: '提示', content: (message || '').replace('\n', '
'), buttons: { ok: { text: '确定', action: okCall } }, boxWidth: '450px', useBootstrap: false, animateFromElement: false }); return false; }; /** * 显示确认弹窗 * @param {any} message -- 确认消息 * @param {any} okCallback -- 确认执行回调 * @param {any} cancelCallback -- 取消后回调 * @param {any} title -- 弹窗标题 */ main.confirm = function (message, okCallback, cancelCallback, title) { $.confirm({ title: title || '确认?', content: message, type: 'red', buttons: { ok: { text: "确认", btnClass: 'btn-primary', keys: ['enter'], action() { if (okCallback) { var tempCall = okCallback; //确保只执行一次 okCallback = null; tempCall(); } } }, cancel: { text: "取消", action() { cancelCallback && cancelCallback(); } } }, useBootstrap: false, boxWidth: '450px', animateFromElement: false }); }; //使用$.Deferred对象处理 main.confirmV2 = function (message, title) { var deferred = $.Deferred(); $.confirm({ title: title || '确认?', content: message, type: 'red', buttons: { ok: { text: "确认", btnClass: 'btn-primary', keys: ['enter'], action: deferred.resolve }, cancel: { text: "取消", action: deferred.reject } }, useBootstrap: false, boxWidth: '450px', animateFromElement: false }); return deferred.promise(); }; main.waitMe = function (opt) { if (typeof opt === 'boolean') { opt = opt ? 'show' : 'hide'; } else { opt = opt || "show"; } $(document.body).waitMe(opt); }; function __getWaitMeFunc(options) { var waitMeFunc = main.waitMe; if (typeof options !== 'undefined') { var m = options.waitMe; if (typeof m === 'undefined') { m = options; } if (!m) { waitMeFunc = function () { }; } else if (typeof m === 'function') { waitMeFunc = m; } } return waitMeFunc; } /** * ⚠️【全局陷阱 / 接手必读】__post 的成败路由: * - 成功(resp.success 为真 且 非 errcode) -> deferred.resolve(resp) (.done 触发) * - 业务错误(resp.success===false / errcode) -> deferred.reject(resp) (.fail 触发,.done 不触发!) * - 网络异常(ajax error) -> deferred.reject(resp) (.fail 触发) * 即:业务错误走的是 reject,不是 resolve。调用方若只在 .done(...) 里写「业务失败/算不出来」的处理, * 业务失败时那段代码根本不会执行 —— 这是 bug 静默失效的高发地。 * 典型实例:swapTradeEdit.js 的 calcBondForItem「约定2补充(计算器算不出→清另两字段+清标识)」曾因 * applyBondCalcFailure 只写在 .done 内、长期不生效;后改到 .fail 才修复(见该文件注释)。 * 凡要区分「成功 / 业务失败 / 网络异常」的逻辑,务必在 .done 之外再挂 .fail。 */ function __post(url, data, deferred, options) { if (!url) throw "post url 不能为空"; var waitMeFunc = __getWaitMeFunc(options); var ajaxOptions = { type: "POST", data: data, dataType: "json", cache: false, success(resp) { if (options && options.dataType && options.dataType !== "json") { deferred.resolve(resp); return; } if (resp.success === false || resp.errcode) { let alert = options && options.alertFn ? options.alertFn : main.alert; let errmsg = (resp.msg || resp.errmsg || "错误").replace(/\n/g, '

'); alert(errmsg); deferred.reject(resp); } else { resp.msg && main.message(resp.msg); deferred.resolve(resp); } }, beforeSend(jqXHR) { waitMeFunc(true); }, complete(jqXHR, textStatus) { waitMeFunc(false); }, error(jqXHR, textStatus, errorThrown) { options && options.suppressError || main.alert('请求失败'); console.error("请求失败,url:" + url + ",status:" + textStatus + ",error:" + errorThrown); } }; if (data instanceof FormData) { ajaxOptions.processData = false; ajaxOptions.contentType = false; } ajaxOptions = $.extend(ajaxOptions, options); $.ajax(url, ajaxOptions).fail(deferred.reject); } /** * 封装$.ajax方法 * @param {any} options * -- boolean:是否显示加载等待 * -- function:自定义加载等待调用 * -- object:{waitMe,suppressError} * @param {boolean} options.waitMe -- 是否显示加载等待 * @param {boolean} options.suppressError -- 禁止出错时提示 * @returns {jQuery} -- jquery延迟对象Deferred */ main.ajax = function (options) { var deferred = $.Deferred(); var waitMeFunc = __getWaitMeFunc(options); var ajaxOptions = { type: "POST", dataType: "json", success(resp) { if (resp.success === false) { main.alert(resp.msg || "错误"); deferred.reject(resp); } else { main.message(resp.msg || "成功"); deferred.resolve(resp); } }, beforeSend: function () { waitMeFunc(true); }, complete: function () { waitMeFunc(false); }, error(jqXHR, textStatus, errorThrown) { let error = "请求失败,url:" + url + ",status:" + textStatus + ",error:" + errorThrown; (options.suppressError ? console.error : main.alert)(error); } }; ajaxOptions = $.extend(ajaxOptions, options); $.ajax(ajaxOptions).fail(deferred.reject); return deferred.promise(); }; /** * 封装$.Post * @param {any} url -- post url * @param {any} data -- post data * @param {any} options * -- boolean:是否显示加载等待 * -- function:自定义加载等待调用 * -- object:{waitMe,suppressError} * @param {boolean} options.waitMe -- 是否显示加载等待 * @param {boolean} options.suppressError -- 禁止出错时提示 * @returns {jQuery} -- jquery延迟对象Deferred */ // ⚠️ 见上方 __post 说明:本方法业务错误/网络异常走 reject,调用方需 .fail 才能捕获(只 .done 会漏掉失败分支)。 main.post = function (url, data, options) { var d = $.Deferred(); __post(url, data, d, options); return d.promise(); }; /** * main.postSafe — 安全版 main.post,返回标准 Promise 而非 jQuery Deferred。 * ============================================================================ * 解决 main.post 的「业务错误走 reject、.done 不触发」陷阱(见 __post 注释)。 * * 用法: * main.postSafe('/api/calc', { price: 100 }) * .then(function(resp) { /* 成功(含 resp.success !== false)*\/ }) * .catch(function(resp) { /* 业务错误 / 网络异常 *\/ }); * * 或者 async/await: * try { * var resp = await main.postSafe('/api/calc', { price: 100 }); * // 成功 * } catch (resp) { * // 业务错误 / 网络异常(resp 可能是 { success:false, msg:'...' } 或 Error) * } * * 与 main.post 的区别: * - main.post 返回 jQuery Deferred:.done = 成功, .fail = 失败 * - main.postSafe 返回 Promise: .then = 成功, .catch = 失败 * - 行为一致,但 Promise 更符合标准,async/await 更自然 * * @param {string} url -- post url * @param {any} data -- post data * @param {any} options -- 同 main.post * @returns {Promise} -- 成功 resolve(resp),失败 reject(resp) * ============================================================================ */ main.postSafe = function (url, data, options) { return new Promise(function (resolve, reject) { main.post(url, data, options) .done(function (resp) { resolve(resp); }) .fail(function (resp) { reject(resp); }); }); }; //确认提交 main.confirmPost = function (confirmText, url, data, options) { var d = $.Deferred(); var okCallback = function () { __post(url, data, d, options); }; var cancelCallback = function () { d.reject({ cancel: true }); }; main.confirm(confirmText, okCallback, cancelCallback, "提示"); return d.promise(); }; //以提交表单的方式打开新页面 main.formOpenNewPage = function (url, postData) { var $f = $('
') _.each(postData, (val, key) => { if ($.isArray(val)) { _.each(val, (v, i) => { $f.append(''); }) } else { $f.append(''); } }); $f.appendTo("body").submit(); $f.remove(); }; //转换为string main.toString = function (val) { if (typeof val === 'undefined' || val === null) { return ""; } return val.toString(); } /** * 封装表单操作(表单验证+表单提交) * @param {any} options -- 选项,可以直接传入form对象 * @param {any} options.el -- form元素或者jquery对象或者jquery选择器 * @param {any} options.submit -- 默认true,内部自动生成ajax post提交处理,如果为false则不自动提交 * 自定义提交选项: * { * url: string,提交地址,默认选用form元素的action属性或window.location.href, * before: function,提交前的回调,返回false则中止提交 * after: function,提交成功后的回调 * postData: function,获取提交数据,如果未赋值则使用默认的表单序列化函数 * } * @param {any} options.validate -- 默认true,内部自动附加验证器,false则不验证或者传入function对象使用自定义验证 * @returns {any} -- */ main.form = function (options) { if (!options) { return __alert('[错误][main.form]缺少选项'); } !$.isPlainObject(options) && (options = { el: options }); var $form, _hasSaved = 0; function __init() { //jquery form $form = options.el instanceof jQuery ? options.el : $(options.el); if (!$form.is("form")) { $form = $form.find("form:first"); if (!$form.length) { return __alert("[错误][main.form]没有找到form元素"); } } let submit = options.submit; let validate = options.validate; let sumbitHandler = __submitHandle; if (submit === false) { sumbitHandler = function () { return true; }; } else { !$.isPlainObject(submit) && (submit = {}); !$.trim(submit.url) && (submit.url = $.trim($form.attr('action')) || window.location.href); options.submit = submit; } //设置表单验证 if ($form.attr('novalidate') !== undefined || validate === false) { return; } //自定义表单验证 if (typeof validate === "function") { $form.attr("novalidate", true); $form.on("submit.validate", function () { try { validate($form) && sumbitHandler(); } finally { return false; } }); return; } //jquery.validate表单验证 if (typeof validate === "object") { validate = $.extend({ submitHandler: sumbitHandler }, validate); } else { validate = { submitHandler: sumbitHandler }; } main.formValidate($form, validate); } //表单提交处理 function __submitHandle(form) { var submit = options.submit; try { if (typeof submit.before === "function" && submit.before(form) === false) { return false; } if (!submit.url) { return __alert("[错误][main.form]submit.url未配置"); } let data = typeof submit.postData === 'function' ? submit.postData() : $form.serialize(); main.post(submit.url, data).done(function (resp) { _hasSaved += 1; submit.after && submit.after(resp, _hasSaved); }); } catch (e) { console.log(e); } finally { return false; } } $(__init); return { submit() { $form.submit(); } }; }; /** * 表单验证 * @param {any} form -- 表单元素或者jquery对象或者jquery元素选择器 * @param {any} options --jquery validate选项或者submitHandler函数 * @returns {any} -- */ main.formValidate = function (form, options) { if (typeof options === "function") { options = { submitHandler: options }; } if (!form) { return __alert("[main.formValidate]参数错误:form"); } if (!(form instanceof jQuery)) { form = $(form); } //添加自定义验证规则,格式:$method:$param[message]$message[+]$method:$param[message]$message... function __addCustomRule($el) { var rulestr = $.trim($el.data("validate")); if (!rulestr) return; var ruleArr = rulestr.split("[+]"); ruleArr.forEach(function (rulePart) { rulePart = rulePart.trim(); if (!rulePart) return; if (!/^[a-zA-Z]/.test(rulePart)) { console.error("不是正确的验证表达式:" + rulePart); return; } let message, param, method; let index1 = rulePart.indexOf(':'); method = rulePart.substring(0, index2).trim(); if (!method) { console.error("不是有效的验证表达式(缺少method):" + rulePart); return; } let index2 = rulePart.indexOf('[message]'); if (index2 > 0) { message = rulePart.substring(index2 + 9).trim(); } if (index1 > 0) { param = rulePart.substring(index1, index2).trim(); } let rule = {}; rule[method] = param || true; if (message) { rule.messages = {}; rule.messages[method] = decodeURIComponent(message); } $el.rules("add", rule); }); } var validator = form.validate(options); form.find('input').each(function () { var type = $(this).attr("type"); if (type === "text" || type === "password") { __addCustomRule($(this)); } }); return validator; }; /** * 解码HTML * @param {any} inputStr * @returns */ main.decodeHtmlEntity = function (inputStr) { if (typeof inputStr !== 'string') return inputStr; if (!inputStr) return ''; var textarea = document.createElement("textarea"); textarea.innerHTML = inputStr; return textarea.value; }; //jquery.validator (function (factory) { if (typeof define === "function" && define.amd) { define(["jquery", "jquery.validate"], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(require("jquery")); } else { factory(jQuery); } }(function ($) { if (!$.validator) { return $; } $.validator.setDefaults({ errorElement: "span", submitHandler: function () { return true; }, errorPlacement: function (error, element) { error.appendTo(element.parent()); } }); $.validator.addMethod("pattern", function (val, element, param) { if (this.optional(element) || !param) { return true; } val = val.replace(/(^\s*)|(\s*$)/g, ""); return new RegExp("^" + param + "$").test(val); }, "输入不符合要求"); $.extend($.validator.messages, { required: "必填", remote: "请修正此字段", email: "请输入有效的电子邮件地址", url: "请输入有效的网址", date: "请输入有效的日期", dateISO: "请输入有效的日期 (YYYY-MM-DD)", number: "请输入有效的数字", digits: "只能输入数字", creditcard: "请输入有效的信用卡号码", equalTo: "你的输入不相同", extension: "请输入有效的后缀", maxlength: $.validator.format("最多可以输入 {0} 个字符"), minlength: $.validator.format("最少要输入 {0} 个字符"), rangelength: $.validator.format("请输入长度在 {0} 到 {1} 之间的字符串"), range: $.validator.format("请输入范围在 {0} 到 {1} 之间的数值"), step: $.validator.format("请输入 {0} 的整数倍值"), max: $.validator.format("请输入不大于 {0} 的数值"), min: $.validator.format("请输入不小于 {0} 的数值") }); return $; })); //例:$('#form1').serializeObject jQuery.prototype.serializeObject = function () { var obj = new Object(); $.each(this.serializeArray(), function (index, param) { if (param.name in obj) { obj[param.name] = [].concat(obj[param.name], param.value); } else { obj[param.name] = param.value; } }); return obj; }; if (!String.prototype.replaceAll) { String.prototype.replaceAll = function (oldString, newString) { oldString = _.escapeRegExp(oldString); return this.replace(new RegExp(oldString, "gm"), newString) } }