/*---------------------------------------------参数配置---------------------------------------*/ const CONSTANTS = { KEY_CHANNEL_NAMES: 'channel_names', //上游的名字,通道分组用 KEY_MERCHANT_NAMES: 'merchant_names', //下游的名字,机构分组用 KEY_CHANNEL_FAST: 'channel_fast', //上下游余额统计:统计的快充通道 KEY_CHANNEL_NORMAL: 'channel_normal', //上下游余额统计:统计的普充通道 KEY_MERCHANT: 'merchant', //上下游余额统计:统计的机构 KEY_DAILY_CHANNEL: 'daily_channel', //日对账单:统计的通道 KEY_DAILY_MERCHANT: 'daily_merchant', //日对账单:统计的通道 }; /*---------------------------------------------公共方法---------------------------------------*/ function formatDecimals(number, digit= 2) {//格式化金额 if (typeof number === 'string') { number = Number(number); } if (number === 0) { return '0'; } return parseFloat(number).toFixed(digit); } function isValidKey(obj, key) { if (obj.hasOwnProperty(key)) { if (obj[key] !== null && obj[key] !== undefined && obj[key] !== '') { return true; } } return false; } function deepCloneData(obj) {//数据深拷贝 if (obj === null || typeof obj !== 'object') { return obj; } let clone = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepCloneData(obj[key]); } } return clone; } function sortSysData(sysData, sortData, key) {//sortData数据前置,sysData中未包含在sortData中的数据放在后面 let sortedData = deepCloneData(sortData); const unselectedData = sysData.filter(function (sys) { const selected = sortData.find(function(cust) { return sys[key] === cust[key] && sys.tag === sys.tag; }); return !selected; }); return sortedData.concat(unselectedData); } /** * @param sortCustSet 排好序的元素 * @param data 实时数据,未排序 * @param groupKey 分组字段 * @param nameKey 过滤字段,机构和通道均包含name字段,增加nameKey,比如store_name,mch_name,多一层过滤条件防止存在重名情况 * @returns {*[]} */ function sortCustData(sortCustSet, data, groupKey, nameKey) { // 去掉不在当前数据中的排序值 const sortCust = sortCustSet.filter(function (item) { return data.find(function (litem) { return litem.name === item.name && litem[nameKey] === item[nameKey] && litem.tag === item.tag; }); }); const custMap = sortCust.reduce((acc, item, index) => { acc[item.name] = index; return acc; }, {}); data.sort((a, b) => custMap[a.name] - custMap[b.name]); const groupMap = {}; data.forEach(item => { const group = item[groupKey]; if (!groupMap[group]) { groupMap[group] = []; } groupMap[group].push(item); }); const sortedData = []; data.forEach(item => { const group = item[groupKey]; if (groupMap[group]) { sortedData.push(...groupMap[group]); delete groupMap[group]; } }); return sortedData; } /* * 合并二维数据,表格数据合并/通道机构配置项合并 */ function mergeData(data1, data2, key) { let dataMormalMerge = []; for (let item of data1) { const p2Item = data2.find(function(p2) { return item[key] === p2[key] && item.tag === p2.tag; }); if (!p2Item) { dataMormalMerge.push(item); } else { dataMormalMerge.push(p2Item); } } for (let item of data2) { const mItem = dataMormalMerge.find(function(m) { return item[key] === m[key] && item.tag === m.tag; }); if (!mItem) { dataMormalMerge.push(item); } } return dataMormalMerge; } /* * 合并一维数组,上下游名称配置项合并 */ function mergeNames(data1, data2) { const notin = data2.filter(item => !data1.includes(item)); return data1.concat(notin); } function handleInputd2(event) {//限制金额输入框 let element = event.target; let value = element.textContent; let selection = window.getSelection(); let range = selection.getRangeAt(0); let startOffset = range.startOffset; value = value.replace(/[^\d.-]/g, ''); const parts = value.split('.'); if (parts.length > 1) { parts[1] = parts[1].slice(0, 2); value = parts.join('.'); } element.textContent = value; let newNode = element.childNodes[0] || element; range.setStart(newNode, Math.min(startOffset, value.length)); range.setEnd(newNode, Math.min(startOffset, value.length)); selection.removeAllRanges(); selection.addRange(range); } function handleInputd4(event) { let element = event.target; let value = element.textContent; let selection = window.getSelection(); let range = selection.getRangeAt(0); let startOffset = range.startOffset; value = value.replace(/[^\d.-]/g, ''); const parts = value.split('.'); if (parts.length > 1) { parts[1] = parts[1].slice(0, 4); value = parts.join('.'); } element.textContent = value; let newNode = element.childNodes[0] || element; range.setStart(newNode, Math.min(startOffset, value.length)); range.setEnd(newNode, Math.min(startOffset, value.length)); selection.removeAllRanges(); selection.addRange(range); } function isObjectEmpty(obj) { return Object.keys(obj).length === 0; } function showErr(error) { layer.alert(error, { icon: 2, title: '错误' }); } function showSuccess(msg) { layer.msg(msg, { icon: 1, time: 2000 }); } function isNumeric(value) { return /^-?\d+(\.\d+)?$/.test(value); } /*----------------------------------------------统一请求定义---------------------------------------*/ class ApiService { constructor(baseUrl) { this.baseUrl = baseUrl; } request(endpoint, method, data) { const that = this; return new Promise(function(resolve, reject) { $.ajax({ url: that.baseUrl + endpoint, type: method, data: data, dataType: 'json', success: function(response) { if (response.state) { resolve(response); } else { reject(new Error('请求失败')); } }, error: function(jqXHR, textStatus, errorThrown) { reject(new Error('请求失败: ' + textStatus + ', ' + errorThrown)); } }); }); } getStatsSettings() { const endpoint = '?act=refill_amount_stats&op=stats_settings'; return this.request(endpoint, 'GET', {}); }; setStatsSettings(key, value) { const endpoint = '?act=refill_amount_stats&op=stats_settings_save'; let data = { key: key, value: value }; return this.request(endpoint, 'POST', data); }; getSysChannels() { const endpoint = '?act=refill_amount_stats&op=sys_channels'; return this.request(endpoint, 'GET', {}); }; getSysMchs() { const endpoint = '?act=refill_amount_stats&op=sys_mchs'; return this.request(endpoint, 'GET', {}); }; getChannelData(params) { const queryString = $.param(params); const endpoint = '?act=refill_amount_stats&op=channel_data&' + queryString; return this.request(endpoint, 'GET', {}); }; getMchData(params) { const queryString = $.param(params); const endpoint = '?act=refill_amount_stats&op=mch_data&' + queryString; return this.request(endpoint, 'GET', {}); }; getDailyProviderData(params) { const queryString = $.param(params); const endpoint = '?act=refill_amount_stats&op=daily_provider_data&' + queryString; return this.request(endpoint, 'GET', {}); }; getDailyMerchantData(params) { const queryString = $.param(params); const endpoint = '?act=refill_amount_stats&op=daily_merchant_data&' + queryString; return this.request(endpoint, 'GET', {}); }; } const statsApi = new ApiService(''); /*------------------------------------------------------组件实现:调度类-------------------------------------------*/ class ComponentBase { constructor(options) { this.el = document.querySelector(options.el); this.components = options.components || {}; this.componentData = options.componentData || {}; this.events = {}; this.componentInstances = {}; this.render(); } render() { this.el.innerHTML = this.compile(this.el.innerHTML); } compile(template) { const componentNames = Object.keys(this.components); componentNames.forEach(name => { const regex = new RegExp(`<${name}([^>]*)>(.*?)<\/${name}>`, 'g'); template = template.replace(regex, (match, attrs) => { const props = {}; attrs.replace(/(\w+)="([^"]*)"/g, (match, key, value) => { if (CONSTANTS[value] !== undefined) { value = CONSTANTS[value]; } props[key] = value; }); const component = new this.components[name]({ props: props, data: this.componentData[name] || {}, parent: this }); this.componentInstances[name] = component; return component.render(); }); }); return template; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } emit(event, ...args) { if (this.events[event]) { this.events[event].forEach(callback => callback(...args)); } } getComponent(name) { return this.componentInstances[name]; } } /*------------------------------------------------------组件实现:组件基类-------------------------------------------*/ class Component { constructor(options) { this.data = options.data || {}; this.props = options.props || {}; this.template = options.template || ''; this.id = 'component-' + Math.random().toString(36).substr(2, 9); this.props['comp-id'] = this.id; } render() { let template = this.template; for (const key in this.props) { template = template.replace(new RegExp(`{{${key}}}`, 'g'), this.props[key]); } setTimeout(() => { this.bindEvents(); this.mounted(); }, 0); return template; } bindEvents() { const rootElement = document.querySelector(`#${this.id}`); if (!rootElement) { return; } this.unbindEvents(); const supportedEvents = ['click', 'input', 'blur', 'change']; supportedEvents.forEach(eventType => { const elements = rootElement.querySelectorAll(`[v-on\\:${eventType}]`); elements.forEach(element => { const method = element.getAttribute(`v-on:${eventType}`); if (method && typeof this[method] === 'function') { const listener = this[method].bind(this); element.addEventListener(eventType, listener); if (!this.eventListeners[eventType]) { this.eventListeners[eventType] = []; } this.eventListeners[eventType].push({ element, listener }); } }); }); } unbindEvents() { for (const eventType in this.eventListeners) { this.eventListeners[eventType].forEach(({ element, listener }) => { element.removeEventListener(eventType, listener); }); } this.eventListeners = {}; } mounted() { } } /*------------------------------------------------------layui表格基类-----------------------------------------------*/ class LayuiTableBase { constructor(id) { this.tableId = id; } /* * 表格中可编辑列的处理 */ editEvent(editColumns = [], callback) { const tableItem = $('#' + this.tableId); const trs = tableItem.next('.layui-table-view').find('.layui-table-body tbody tr'); trs.each(function (i, tr) { for (let col of editColumns) { const td = $(tr).find('td[data-field="' + col + '"]').not('.layui-hide'); if (td.length > 0) {//合并隐藏的不处理 const valDiv = td.find('div'); valDiv.attr('contenteditable', true).css('background-color', 'lightblue'); valDiv.off('input', handleInputd4) .off('keypress') .off('blur') .on('input', handleInputd4) .on('keypress', function (e) { if (e.keyCode === 13) { e.preventDefault(); $(this).blur(); } }) .on('blur', function () { const val = valDiv.text().trim(); if (!isNumeric(val)) { valDiv.html('0'); } if (callback !== null && typeof callback === 'function') { const index = $(this).parent().parent().data('index'); callback(index); } }); } } }); } switchEvent(switchColumns = [], callback) { const tableItem = $('#' + this.tableId); const trs = tableItem.next('.layui-table-view').find('.layui-table-body tbody tr'); trs.each(function (i, tr) { for (let clo of switchColumns) { const td = $(tr).find('td[data-field="' + clo + '"]').not('.layui-hide'); if (td.length > 0) { const buttons = td.find('.layui-btn'); buttons.off('click').on('click', function (e) { e.preventDefault(); e.stopPropagation(); const currTr = $(this).closest('tr'); const index = currTr.data('index'); buttons.removeClass('selected').addClass('unselected'); $(this).removeClass('unselected').addClass('selected'); if (callback !== null && typeof callback === 'function') { callback(index); } }); } } }); } /* * 表格中金额字段样式,小于0的数值设置为红色 */ setValueClass(valColumns) { const trs = $('#' + this.tableId).next('.layui-table-view').find('.layui-table-body tbody tr'); trs.each(function(i, tr) { for(let col of valColumns) { const td = $(tr).find('td[data-field="' + col + '"]').not('.layui-hide'); if (td.length > 0) {//合并隐藏的不处理 const valDiv = td.find('div'); const val = valDiv.text().trim(); if (parseFloat(val) < 0) { valDiv.addClass('negative-value'); } else { valDiv.removeClass('negative-value'); } } } }); } } /*------------------------------------------------------组件:上下游名称设置-------------------------------------------*/ class StreamNameComponent extends Component { constructor(options) { super(Object.assign(options, { template: `
{{title}}:
` })); this.cacheKey = this.props['type']; this.cacheData = []; } async mounted() { await this.loadData(); this.updateUI(); } async open() { const content = this.createModelContent(); $('#group-names-model-' + this.id + ' .names-manager-container').html(content); this.bindEvents(); layer.open({ type: 1, title: '设置上游名称', content: $('#group-names-model-' + this.props['comp-id']), area: ['500px', '400px'], success: function() { } }); } async loadData() { const statsSettings = await statsApi.getStatsSettings(); if (statsSettings.state === true) { this.cacheData = statsSettings[this.cacheKey] || []; } } createModelContent() { const channelNames = this.cacheData; let html = ''; for (let i = 0; i < channelNames.length; i++) { html += ''; } return html; } save() { const that = this; const inputVal = $('#input-' + this.id).val().replace(/,/g, ','); const newNames = inputVal.split(',').map(item => item.trim()).filter(item => item); if (newNames.length > 0) { const cache = this.cacheData; const updatedNames = cache.concat(newNames); const uniqueNames = updatedNames.filter((item, index) => updatedNames.indexOf(item) === index); const loadIndex = layer.load(2, { shade: [0.2, '#000'] }); that.updateCache(uniqueNames, [loadIndex]); } } delete(event) { const that = this; const index = event.currentTarget.getAttribute('data-index'); const cache = this.cacheData; const tmpNames = deepCloneData(cache); const tag = tmpNames[index]; layer.confirm('确定要删除 "' + tag + '" 吗?', { title: '删除确认', icon: 3, btn: ['删除', '取消'], btn1: function (btn_i) { tmpNames.splice(index, 1); const loadIndex = layer.load(2, { shade: [0.2, '#000'] }); that.updateCache(tmpNames, [loadIndex, btn_i]); } }); } updateCache(tmpNames, closer = []) { const that = this; const cacheKey = this.cacheKey; statsApi.setStatsSettings(cacheKey, tmpNames) .then(function (data) { closer.forEach(function (lr) { layer.close(lr); }); if (data.state === true) { that.cacheData = data[cacheKey]; that.updateUI(); const content = that.createModelContent(); $('#group-names-model-' + that.id + ' .names-manager-container').html(content); $('#input-' + that.id).val(''); that.bindEvents(); } else { showErr('保存失败,请重试'); } }) .catch(function (error) { showErr('保存失败,请重试'); }); } updateUI() { const selector = $('#names-selected-' + this.id); const cache = this.cacheData; let html = ''; for(let item of cache) { html += ''; } selector.html(html); } } /*-------------------------------------------------------组件:设置统计的通道/机构-----------------------------------------*/ class SelectedChannelComponent extends Component { constructor(options) { super(Object.assign(options, { template: `
` })); this.parent = options.parent; this.cacheKey = this.props['type']; this.cacheData = []; //计入统计的通道 this.lastCacheData = []; //打开弹层时的数据,用于对比是否做了修改 this.sysData = []; //系统全量通道,初始时为空,数据在第一次打开弹层时加载 } mounted() { const that = this; $('#selected-items-' + this.id).sortable({ containment: "parent", cursor: "move", update: function(event, ui) { that.updateCheckboxOrder(); } }).disableSelection(); } async open() { await this.loadData(); this.renderForm(true); const that = this; layer.open({ type: 1, title: this.props['title'], area: ['960px', '600px'], content: $('#select-modal-' + this.id), btn: ['确定', '取消'], yes: function() { if (that.isEdit() === true) { statsApi.setStatsSettings(that.cacheKey, that.cacheData) .then(function (data) { layer.closeAll(); }) .catch(function (error) { layer.closeAll(); showErr('保存失败,请重试'); }); } else { layer.closeAll(); } }, end: function() { if (that.isEdit() === true) { if (that.parent && typeof that.parent.emit === 'function') { that.parent.emit(that.props['emit'], that.cacheKey, that.cacheData); } } } }); } async loadData() { const loadIndex = layer.load(2, { shade: [0.2, '#000'] }); const sysChannels = await statsApi.getSysChannels(); if (sysChannels.state === true) { this.sysData = sysChannels.list; } const statsSettings = await statsApi.getStatsSettings(); this.cacheData = statsSettings[this.cacheKey] || []; this.sysData = sortSysData(this.sysData, this.cacheData, 'store_name'); this.lastCacheData = deepCloneData(this.cacheData); layer.close(loadIndex); } isEdit() { if (this.lastCacheData.length !== this.cacheData.length) { return true; } for (let i = 0; i < this.lastCacheData.length; i++) { const u1 = this.cacheData[i].tag + '-' + this.cacheData[i].store_id; const u2 = this.lastCacheData[i].tag + '-' + this.lastCacheData[i].store_id; if (u1 !== u2) { return true; } } return false; } renderForm(updateSelectedItems) { let formHtml = ''; for (let item of this.sysData) { let checked = ''; const dataItem = this.cacheData.find(function (chan) { return chan.store_name === item.store_name && chan.tag === item.tag; }); if (dataItem) { checked = 'checked="true"'; } formHtml += ''; } $('#select-form-' + this.id).html(formHtml); this.bindEvents(); if (updateSelectedItems === true) { this.updateSelectedItems(); } } change(event) { const tag = event.currentTarget.getAttribute('data-tag'); const value = event.currentTarget.value; const checked = event.currentTarget.checked; this.updateCache(value, tag, checked); this.updateSelectedItems(); } updateCache(storeName, tag, checked) { if (checked === true) { const item = this.sysData.find(function (chan) { return chan.store_name === storeName && chan.tag === tag; }); if (item) { this.cacheData.push(item); } } else { let cusChannelsNew = []; for (let item of this.cacheData) { if (item.store_name !== storeName || (item.store_name === storeName && item.tag !== tag)) { cusChannelsNew.push(item); } } this.cacheData = cusChannelsNew; } } updateSelectedItems() { const selectedItems = $('#' + this.id).find('#selected-items-' + this.id); selectedItems.empty(); for(let item of this.cacheData) {//选中项放入排序区 const itemHtml = '
' + item.store_name + '
'; selectedItems.append(itemHtml); } this.updateCheckboxOrder(); } updateCheckboxOrder() { const selectedItems = $('#' + this.id).find('#selected-items-' + this.id + ' .sortable-item'); let sortedData = []; const that = this; selectedItems.each(function() { const value = $(this).text().trim(); const tag = $(this).data('tag'); const dataItem = that.sysData.find(function (item) { return item.store_name === value && item.tag === tag; }); if (dataItem) { sortedData.push(dataItem); } }); this.cacheData = deepCloneData(sortedData); //全局通道数据,已选中项,排在前面 const unselectedData = that.sysData.filter(function (item) { return !sortedData.includes(item); }); sortedData = sortedData.concat(unselectedData); this.sysData = sortedData; this.renderForm(false); } } /*-------------------------------------------------------统计的机构设置---------------------------------------------*/ class SelectedMerchantComponent extends Component { constructor(options) { super(Object.assign(options, { template: `
` })); this.parent = options.parent; this.cacheKey = this.props['type']; this.cacheData = []; //计入统计的机构 this.lastCacheData = []; //打开弹层时的数据,用于对比是否做了修改 this.sysData = []; //系统全量通道,初始时为空,数据在第一次打开弹层时加载 } mounted() { const that = this; $('#selected-items-' + this.id).sortable({ containment: "parent", cursor: "move", update: function(event, ui) { that.updateCheckboxOrder(); } }).disableSelection(); } async open() { await this.loadData(); this.renderForm(true); const that = this; layer.open({ type: 1, title: this.props['title'], area: ['960px', '600px'], content: $('#select-modal-' + this.id), btn: ['确定', '取消'], yes: function() { if (that.isEdit() === true) { statsApi.setStatsSettings(that.cacheKey, that.cacheData) .then(function (data) { layer.closeAll(); }) .catch(function (error) { layer.closeAll(); showErr('保存失败,请重试'); }); } else { layer.closeAll(); } }, end: function() { if (that.isEdit() === true) { if (that.parent && typeof that.parent.emit === 'function') { that.parent.emit(that.props['emit'], that.cacheKey, that.cacheData); } } } }); } async loadData() { const loadIndex = layer.load(2, { shade: [0.2, '#000'] }); if (this.sysData.length === 0) { //系统全量机构数据,仅加载1次 const sysMchs = await statsApi.getSysMchs(); if (sysMchs.state === true) { this.sysData = sysMchs.list; } } const statsSettings = await statsApi.getStatsSettings(); this.cacheData = statsSettings[this.cacheKey] || []; this.sysData = sortSysData(this.sysData, this.cacheData, 'mch_name'); this.lastCacheData = deepCloneData(this.cacheData); layer.close(loadIndex); } isEdit() { if (this.lastCacheData.length !== this.cacheData.length) { return true; } for (let i = 0; i < this.lastCacheData.length; i++) { const u1 = this.cacheData[i].tag + '-' + this.cacheData[i].mch_id; const u2 = this.lastCacheData[i].tag + '-' + this.lastCacheData[i].mch_id; if (u1 !== u2) { return true; } } return false; } renderForm(updateSelectedItems) { let formHtml = ''; for (let item of this.sysData) { let checked = ''; const dataItem = this.cacheData.find(function (chan) { return chan.mch_name === item.mch_name && chan.tag === item.tag; }); if (dataItem) { checked = 'checked="true"'; } formHtml += ''; } $('#select-form-' + this.id).html(formHtml); this.bindEvents(); if (updateSelectedItems === true) { this.updateSelectedItems(); } } change(event) { const tag = event.currentTarget.getAttribute('data-tag'); const value = event.currentTarget.value; const checked = event.currentTarget.checked; this.updateCache(value, tag, checked); this.updateSelectedItems(); } updateCache(mchName, tag, checked) { if (checked === true) { const item = this.sysData.find(function (chan) { return chan.mch_name === mchName && chan.tag === tag; }); if (item) { this.cacheData.push(item); } } else { let cusMchsNew = []; for (let item of this.cacheData) { if (item.mch_name !== mchName || (item.mch_name === mchName && item.tag !== tag)) { cusMchsNew.push(item); } } this.cacheData = cusMchsNew; } } updateSelectedItems() { const selectedItems = $('#' + this.id).find('#selected-items-' + this.id); selectedItems.empty(); for(let item of this.cacheData) {//选中项放入排序区 const itemHtml = '
' + item.mch_name + '
'; selectedItems.append(itemHtml); } this.updateCheckboxOrder(); } updateCheckboxOrder() { const selectedItems = $('#' + this.id).find('#selected-items-' + this.id + ' .sortable-item'); let sortedData = []; const that = this; selectedItems.each(function() { const value = $(this).text().trim(); const tag = $(this).data('tag'); const dataItem = that.sysData.find(function (item) { return item.mch_name === value && item.tag === tag; }); if (dataItem) { sortedData.push(dataItem); } }); this.cacheData = deepCloneData(sortedData); //全局通道数据,已选中项,排在前面 const unselectedData = that.sysData.filter(function (item) { return !sortedData.includes(item); }); sortedData = sortedData.concat(unselectedData); this.sysData = sortedData; this.renderForm(false); } }