New file |
| | |
| | | /** |
| | | * Fetcher |
| | | * @author Tevin |
| | | */ |
| | | |
| | | import Axios from 'axios'; |
| | | import Qs from 'qs'; |
| | | import {message, Modal} from 'antd'; |
| | | import {Tools} from '@components/common/Tools'; |
| | | |
| | | export class Fetcher { |
| | | |
| | | /** |
| | | * @constructor |
| | | * @param {Object} options |
| | | */ |
| | | constructor(options = {}) { |
| | | this._data = { |
| | | urlPrefix: options.urlPrefix || ['/api/common', '/api/common'], |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * 请求配置 |
| | | * @type {{headers: Object, baseURL: string, responseType: string, timeout: number}} |
| | | * @private |
| | | */ |
| | | _defaultConfig = { |
| | | baseURL: window.location.protocol + '//' + window.location.host, |
| | | headers: { |
| | | 'X-Requested-With': 'XMLHttpRequest', |
| | | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', |
| | | 'Ax-Rq-Type': 'separation', |
| | | }, |
| | | responseType: 'json', |
| | | timeout: 10000, |
| | | }; |
| | | |
| | | /** |
| | | * 拼写 URL 地址 |
| | | * @param {String} devSuffix |
| | | * @param {String} [serSuffix] |
| | | * @return {String} |
| | | */ |
| | | spellURL(devSuffix, serSuffix) { |
| | | let url = ''; |
| | | // 开发环境地址 |
| | | if (Fetcher.inDevMod) { |
| | | url = this._data.urlPrefix[0] + devSuffix; |
| | | } |
| | | // 生产环境地址 |
| | | else { |
| | | url = this._data.urlPrefix[1] + (serSuffix || devSuffix); |
| | | } |
| | | const fixReg = /[a-zA-Z0-9]+\/\.\.\//; |
| | | while (url.indexOf('../') >= 0) { |
| | | url = url.replace(fixReg, ''); |
| | | } |
| | | url = url.replace(/[./]\//g, ''); |
| | | return url; |
| | | } |
| | | |
| | | /** |
| | | * 将 post 参数转换为 get 参数,并拼接 url |
| | | * @param url |
| | | * @param data |
| | | */ |
| | | parseParamUrl(url, data) { |
| | | const params = Qs.stringify(data); |
| | | if (url.indexOf('?') >= 0) { |
| | | url += '&' + params; |
| | | } else { |
| | | url += '?' + params; |
| | | } |
| | | return url; |
| | | } |
| | | |
| | | /** |
| | | * get 请求 |
| | | * @param {String} url |
| | | * @param {*} data |
| | | * @param {(String[])[]} [remap] |
| | | * @param {object} [options] |
| | | * @return {Promise<any>} |
| | | */ |
| | | get(url, data, remap = [], options = null) { |
| | | const url2 = this.parseParamUrl(url, data); |
| | | return this.query('get', url2, null, remap, options); |
| | | } |
| | | |
| | | /** |
| | | * post 请求 |
| | | * @param {String} url |
| | | * @param {*} data |
| | | * @param {(String[])[]} [remap] |
| | | * @param {object} [options] |
| | | * @return {Promise<any>} |
| | | */ |
| | | post(url, data, remap = [], options = null) { |
| | | // mock 模式 |
| | | if (Fetcher.inMockMod) { |
| | | return this.get(url, data, remap, options); |
| | | } |
| | | // 正常模式 |
| | | const params = Qs.stringify(data); |
| | | return this.query('post', url, params, remap, options); |
| | | } |
| | | |
| | | /** |
| | | * 基础 ajax 请求 |
| | | * @param {String} type |
| | | * @param {String} url |
| | | * @param {*} [data] |
| | | * @param {*} [remap] |
| | | * @param {object} [options] |
| | | * @return {Promise<any>|} |
| | | */ |
| | | query(type, url, data = null, remap, options) { |
| | | if (!type || !/^get|post|put$/i.test(type) || !url) { |
| | | return Promise.reject(); |
| | | } |
| | | const method = type.toLowerCase(); |
| | | const response = Axios[method](url, data, { |
| | | ...this._defaultConfig, |
| | | ...options, |
| | | }); |
| | | return response |
| | | .then(response => { |
| | | /** |
| | | * @type {{state: {code, http, msg}, data: Object}} |
| | | * @example response.state.code |
| | | * 2000 通用请求成功 |
| | | * 2001 请求成功,但是没有数据,弹窗提示 msg(仅特殊情况使用) |
| | | * 5000 通用请求失败,弹窗提示 msg |
| | | * 9001 登陆已过期,弹窗提示过期且返回登陆页 |
| | | * 9002 已登陆但没有操作权限,弹窗提示 msg |
| | | */ |
| | | const responseData = this._adaptiveResponseData(response.data); |
| | | responseData.state.http = response.status; |
| | | return this._transformResponseData(responseData, remap); |
| | | }) |
| | | // HTTP 异常 |
| | | .catch((err) => { |
| | | this._resolveCaughtNetErr(err); |
| | | console.log('query error', err); |
| | | return null; |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 适配旧版响应体,转换为新版 |
| | | * @private |
| | | */ |
| | | _adaptiveResponseData(responseData) { |
| | | // 标准请求,不转换 |
| | | if (typeof responseData.state === 'object' && typeof responseData.data === 'object') { |
| | | return responseData; |
| | | } |
| | | // 旧请求,操作类通讯,响应体转换 |
| | | if (typeof responseData.status !== 'undefined' && typeof responseData.dataMsg !== 'undefined') { |
| | | // 转换数据体 |
| | | let data2 = {rows: []}; |
| | | // 数组类型 |
| | | if (responseData.dataMsg instanceof Array) { |
| | | if (responseData.dataMsg.length > 0) { |
| | | data2.rows = responseData.dataMsg; |
| | | } |
| | | } |
| | | // 对象类型 |
| | | else if (responseData.dataMsg instanceof Object) { |
| | | if (!Tools.isEmptyObject(responseData.dataMsg)) { |
| | | data2 = responseData.dataMsg; |
| | | } |
| | | } |
| | | // 合并响应体 |
| | | return { |
| | | state: { |
| | | code: responseData.status === 200 ? 2000 : 5000, |
| | | msg: responseData.msg, |
| | | }, |
| | | data: data2, |
| | | }; |
| | | } |
| | | // 旧版请求,数据列表类通讯,响应体转换 |
| | | if (typeof responseData.data !== 'undefined' && typeof responseData.count !== 'undefined') { |
| | | const data = (!!responseData.data && typeof responseData.data !== 'object') ? |
| | | {data: responseData.data} : null; |
| | | return { |
| | | state: { |
| | | code: responseData.code === 0 ? 2000 : 5000, |
| | | msg: responseData.msg, |
| | | }, |
| | | data: { |
| | | rows: responseData.data || [], |
| | | ...data, |
| | | total: responseData.count, |
| | | ext: responseData.ext, |
| | | }, |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 解析捕获的网络错误 |
| | | * @param err |
| | | * @private |
| | | */ |
| | | _resolveCaughtNetErr(err) { |
| | | let msg = ''; |
| | | if (err.response && err.response.status) { |
| | | switch (err.response.status) { |
| | | case 400: |
| | | msg += '通讯请求有误!(400 Bad Request)'; |
| | | break; |
| | | case 401: |
| | | msg += '您的登陆已失效!请重新登陆!(401 Unauthorized)'; |
| | | break; |
| | | case 403: |
| | | msg += '通讯请求被拒绝!(403 Forbidden)'; |
| | | break; |
| | | case 404: |
| | | msg += '通讯请求不存在!(404 Not Found)'; |
| | | break; |
| | | case 405: |
| | | msg += '通讯请求不允许访问!(405 Method Not Allowed)'; |
| | | break; |
| | | case 500: |
| | | msg += '通讯服务器处理异常!(500 Internal Server Error)'; |
| | | break; |
| | | case 502: |
| | | msg += '通讯网关异常!(502 Bad Gateway)'; |
| | | break; |
| | | case 503: |
| | | msg += '通讯服务器维护中/已过载!(503 Service Unavailable)'; |
| | | break; |
| | | case 504: |
| | | msg += '通讯网关已超时!(504 Gateway Timeout)'; |
| | | break; |
| | | default: |
| | | msg += '网络通讯异常!(' + err.message + ')'; |
| | | break; |
| | | } |
| | | } else { |
| | | msg += '解析通讯数据异常!'; |
| | | } |
| | | message.error(msg); |
| | | } |
| | | |
| | | /** |
| | | * 转换响应体 |
| | | * @param response |
| | | * @param {Array[]} remap |
| | | * @returns {Object|{}|null} |
| | | * @private |
| | | */ |
| | | _transformResponseData(response, remap) { |
| | | if (!response) { |
| | | return null; |
| | | } |
| | | if (response.state.code === 2000) { |
| | | if (!response.data) { |
| | | return {}; |
| | | } else { |
| | | // 允许通用列表为 null |
| | | if (typeof response.data.rows !== 'undefined' && !Tools.isArray(response.data.rows)) { |
| | | response.data.rows = []; |
| | | } |
| | | // 先转驼峰 |
| | | response.data = this.transKeyName('camel', response.data); |
| | | // 再重映射 |
| | | if (remap && remap.length > 0) { |
| | | response.data = this._remapData(response.data, remap); |
| | | } |
| | | // 转换常规数字字符串为数值 |
| | | response.data = this._transNumStringToNumber(response.data); |
| | | return response.data; |
| | | } |
| | | } else if (response.state.code === 2001) { |
| | | message.info(response.state.msg); |
| | | return null; |
| | | } else if (response.state.code === 9001) { |
| | | this._showLoginExpired(); |
| | | } else { |
| | | message.error(response.state.msg); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 转换响应体数据结构 |
| | | * @param {Object} data |
| | | * @param {Array[]} maps |
| | | * @example maps: [ |
| | | * ['rows.[]', 'recvName', 'userName'] // 默认语法:键名转换(路径,旧名,新名) |
| | | * ] |
| | | * @private |
| | | */ |
| | | _remapData(data, maps) { |
| | | // 渡值 |
| | | const ferryValue = (source, paths, map) => { |
| | | // 最后一环,传值 |
| | | if (paths.length === 0) { |
| | | // 目标已有值,跳过传值不覆盖 |
| | | if (typeof source[map[2]] !== 'undefined') { |
| | | return; |
| | | } |
| | | // 来源没有值,赋值空字符串 |
| | | if (typeof source[map[1]] === 'undefined') { |
| | | source[map[2]] = ''; |
| | | } |
| | | // 来源有值,直接赋值 |
| | | else { |
| | | source[map[2]] = source[map[1]]; |
| | | } |
| | | delete source[map[1]]; |
| | | return; |
| | | } |
| | | // 提取当前环节 |
| | | const curPath = paths.shift(); |
| | | if (curPath === '[]') { |
| | | source.forEach(item => { |
| | | ferryValue(item, [...paths], map); |
| | | }); |
| | | } else { |
| | | ferryValue(source[curPath], [...paths], map); |
| | | } |
| | | }; |
| | | for (let map of maps) { |
| | | // 键名转换 |
| | | if (map[0].indexOf('.') >= 0) { |
| | | const paths = map[0].split('.'); |
| | | ferryValue(data, paths, map); |
| | | } else { |
| | | if (map[0].length > 0) { |
| | | ferryValue(data, [map[0]], map); |
| | | } else { |
| | | ferryValue(data, [], map); |
| | | } |
| | | } |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | /** |
| | | * 下划线字符串转小驼峰 |
| | | * @param {String} str |
| | | * @return {String} |
| | | * @private |
| | | */ |
| | | _stringToCamel(str) { |
| | | let str2 = ''; |
| | | if (str.indexOf('_') <= 0) { |
| | | str2 = str; |
| | | } else { |
| | | let words = str.split('_'); |
| | | for (let i = 1; i < words.length; i++) { |
| | | words[i] = words[i].substr(0, 1).toUpperCase() + words[i].substr(1); |
| | | } |
| | | str2 = words.join(''); |
| | | } |
| | | return str2; |
| | | } |
| | | |
| | | /** |
| | | * 小驼峰字符串转下划线 |
| | | * @param {String} str |
| | | * @return {String} |
| | | * @private |
| | | */ |
| | | _stringToUnderline(str) { |
| | | let str2 = ''; |
| | | if ((/[A-Z]/).test(str)) { |
| | | str2 = str.replace(/([A-Z])/g, ($1) => { |
| | | return '_' + $1.toLowerCase(); |
| | | }); |
| | | } else { |
| | | str2 = str; |
| | | } |
| | | return str2; |
| | | } |
| | | |
| | | /** |
| | | * 驼峰与下划线命名模式转换 |
| | | * @param {String} type - 'camel' or 'underline' |
| | | * @param {Object} json |
| | | * @return {Object} |
| | | */ |
| | | transKeyName(type, json) { |
| | | const transform = (json, json2) => { |
| | | for (let p in json) { |
| | | if (json.hasOwnProperty(p)) { |
| | | let key; |
| | | // 数值键名直接传递 |
| | | if (/^\d+$/.test(p)) { |
| | | key = parseInt(p); |
| | | } |
| | | // 字符串键名进行转换 |
| | | else { |
| | | if (type === 'camel') { |
| | | key = this._stringToCamel(p); |
| | | } else if (type === 'underline') { |
| | | key = this._stringToUnderline(p); |
| | | } |
| | | } |
| | | // 属性为对象时,递归转换 |
| | | if (json[p] instanceof Object) { |
| | | json2[key] = transform(json[p], Tools.isArray(json[p]) ? [] : {}); |
| | | } |
| | | // 属性非对象,为字符串但内容符合json格式,递归转换 |
| | | else if (Tools.isString(json[p]) && /^[{[]+("([a-zA-Z][a-zA-Z0-9\-_]*?)":(.+?))+[}\]]+$/.test(json[p])) { |
| | | json2[key] = JSON.parse(json[p]); |
| | | json2[key] = transform(json2[key], Tools.isArray(json2[key]) ? [] : {}); |
| | | json2[key] = JSON.stringify(json2[key]); |
| | | } |
| | | // 属性非对象,非json字符串,直接传递 |
| | | else { |
| | | json2[key] = json[p]; |
| | | } |
| | | } |
| | | } |
| | | return json2; |
| | | }; |
| | | return transform(json, Tools.isArray(json) ? [] : {}); |
| | | } |
| | | |
| | | /** |
| | | * 转换图片路径(旧版运营平台) |
| | | * @param {String} type - fix or cut |
| | | * @param {String} path |
| | | * @example |
| | | * fix -> '/upload/4/5e6c91eeccedc.jpg' |
| | | * cut -> '4/5e56307c489c7.jpg' |
| | | */ |
| | | transImgPath(type, path) { |
| | | if (!path) { |
| | | return ''; |
| | | } |
| | | if (path.indexOf(',') >= 0) { |
| | | return path.split(',').map(p => this.transImgPath(type, p)).join(','); |
| | | } else { |
| | | // 修复补齐 |
| | | if (type === 'fix') { |
| | | if (!path || /^\/(upload|static)/.test(path)) { |
| | | return path; |
| | | } else { |
| | | return '/upload/' + path; |
| | | } |
| | | } else if (type === 'cut') { |
| | | const pathArr = path.split('upload/'); |
| | | return pathArr[pathArr.length - 1]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 转换简单的数字字符串为数值 |
| | | * @param {*} json |
| | | * @return {*} |
| | | * @private |
| | | */ |
| | | _transNumStringToNumber(json) { |
| | | // 匹配 9 位数字,范围:999999999 ~ 0.0000001,排除正数且0开头的字符串 |
| | | const simpleNumReg = /(^[1-9][\d.]{0,8}$)|(^-\d[\d.]{0,8}$)|(^\d(\.\d{0,7})?$)/; |
| | | // 匹配金额 |
| | | const moneyStrReg = /^-?\d+(,\d+)*\.\d{2}$/; |
| | | const transNumber = (json) => { |
| | | for (let p in json) { |
| | | if (json.hasOwnProperty(p)) { |
| | | // 属性为对象时,递归转换 |
| | | if (json[p] instanceof Object) { |
| | | transNumber(json[p]); |
| | | } |
| | | // 属性非对象,判断是否为简单数字字符串且不是金额,是则转换 |
| | | else { |
| | | if (Tools.isString(json[p]) && simpleNumReg.test(json[p]) && !moneyStrReg.test(json[p])) { |
| | | json[p] = Number(json[p]); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | transNumber(json); |
| | | return json; |
| | | } |
| | | |
| | | /** |
| | | * 记录是否为本地开发模式 |
| | | * @type {Boolean} |
| | | */ |
| | | static inDevMod = (() => { |
| | | // 当处于 mock 请求模式,视为本地开发 |
| | | if (Tools.getTopUrlParam('query') === 'mock') { |
| | | return true; |
| | | } |
| | | // 强制 real 请求,可在本地使用真实请求 |
| | | if (Tools.getTopUrlParam('query') === 'real') { |
| | | return false; |
| | | } |
| | | // 当没有 url 指定时,只有内网 ip 和 35** 的端口号,视为本地开发模式 |
| | | return /^(192|127|localhost).*?:35\d{2}$/i.test(window.location.host); |
| | | })(); |
| | | |
| | | |
| | | } |