diff --git a/common/js/diy.js b/common/js/diy.js index a11a5cd..87149b5 100644 --- a/common/js/diy.js +++ b/common/js/diy.js @@ -352,14 +352,16 @@ export default { this.$util.showToast({ title: res.message }); - this.diyData = {}; + // 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局 + this.diyData = { ...(this.diyData || {}), value: [] }; return; } this.$util.showToast({ title: this.$lang('diy.noDiyData') }); - this.diyData = {}; + // 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局 + this.diyData = { ...(this.diyData || {}), value: [] }; return; } diff --git a/common/js/http.js b/common/js/http.js index df855fe..0ad0506 100644 --- a/common/js/http.js +++ b/common/js/http.js @@ -5,7 +5,6 @@ import Platform from './platform.js' const { appType, appTypeName } = Platform.getPlatformInfo(); -// 惰性加载 Lang,避免与 lang.js 的循环依赖 let _Lang = null; function getLang() { if (!_Lang) { @@ -14,6 +13,116 @@ function getLang() { return _Lang; } +const DEFAULT_TIMEOUT = 15000; +const DEFAULT_MAX_RETRIES = 2; +const RETRY_DELAY_BASE = 2000; + +function _shouldRetry(errMsg) { + if (!errMsg) return false; + if (errMsg === 'request:fail url not in domain list') return false; + if (errMsg === 'request:fail abort') return false; + const retryableErrors = [ + 'request:fail timeout', + 'request:fail connect', + 'request:fail network' + ]; + return retryableErrors.some(err => errMsg.startsWith(err)); +} + +function _classifyError(errMsg, statusCode) { + if (!errMsg && !statusCode) return 'unknown'; + if (errMsg === 'request:fail abort') return 'aborted'; + if (errMsg && errMsg.indexOf('timeout') !== -1) return 'timeout'; + if (errMsg && (errMsg.indexOf('connect') !== -1 || errMsg.indexOf('network') !== -1)) return 'networkError'; + if (statusCode && statusCode >= 500) return 'serverError'; + if (errMsg && errMsg.indexOf('url not in domain list') !== -1) return 'domainNotInList'; + return 'unknown'; +} + +function _sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function _makeRequest(url, method, data, header, dataType, responseType, timeout) { + return new Promise((resolve, reject) => { + uni.request({ + url: url, + method: method, + data: data, + header: header, + dataType: dataType, + responseType: responseType, + timeout: timeout, + success: (res) => { + if (res.statusCode && [200, 500].indexOf(res.statusCode) == -1) { + reject({ errMsg: `request:fail statusCode=${res.statusCode}`, statusCode: res.statusCode }); + return; + } + if (res.data.code == -3 && store.state.siteState > 0) { + store.commit('setSiteState', -3); + Util.redirectTo('/pages_tool/storeclose/storeclose', {}, 'reLaunch'); + return; + } + if (res.data.refreshtoken) { + store.commit('setToken', res.data.refreshtoken); + } + if (res.data.code == -10009 || res.data.code == -10010) { + store.commit('setToken', ''); + store.commit('setMemberInfo', ''); + } + resolve(res.data); + }, + fail: (res) => { + reject(res); + } + }); + }); +} + +async function _requestWithRetry(params, url, method, data, header, dataType, responseType) { + const timeout = params.timeout || DEFAULT_TIMEOUT; + const maxRetries = params.noRetry ? 0 : (params.maxRetries || DEFAULT_MAX_RETRIES); + let lastError = null; + let lastStatusCode = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const result = await _makeRequest(url, method, data, header, dataType, responseType, timeout); + return result; + } catch (err) { + lastError = err; + lastStatusCode = err && err.statusCode; + if (attempt >= maxRetries || !_shouldRetry(err.errMsg)) { + break; + } + const delay = RETRY_DELAY_BASE * Math.pow(2, attempt); + await _sleep(delay); + } + } + + if (params.silentError) { + throw lastError; + } + + const errorType = _classifyError(lastError && lastError.errMsg, lastStatusCode); + const lang = getLang(); + if (errorType === 'domainNotInList') { + uni.showToast({ + title: lang.lang('http.domainNotInList', { url: Config.baseUrl }), + 'icon': 'none', + duration: 10000 + }); + } else { + const i18nKey = 'http.' + errorType; + uni.showToast({ + title: lang.lang(i18nKey), + 'icon': 'none', + duration: 10000 + }); + } + throw lastError; +} + export default { sendRequest(params) { if (!Config.baseUrl) { @@ -25,13 +134,9 @@ export default { return; } - // remote-only boot 门控: 所有业务请求必须等国际化数据就绪后才发送 - // 这样业务请求返回时 lang() 已有翻译数据,不会造成 UI 污染 - // bundle 请求走 lang-loader.js 的 uni.request 直接发送,不受此门控影响 const _isRemoteOnly = getLang().isRemoteOnly && getLang().isRemoteOnly(); const _bootDone = getLang().isBootCompleted && getLang().isBootCompleted(); if (_isRemoteOnly && !_bootDone) { - // 获取 boot promise(优先从 App 实例拿,避免重复 boot) let bootPromise = null; try { const app = getApp(); @@ -40,10 +145,8 @@ export default { if (!bootPromise) bootPromise = getLang().boot(); if (params.async === false) { - // 同步 Promise 模式: 链式等待 boot 后再发请求 return bootPromise.then(() => this._sendRequestNow(params)); } else { - // 回调模式: boot 完成后再执行请求 bootPromise.then(() => this._sendRequestNow(params)); return; } @@ -61,11 +164,9 @@ export default { app_type_name: appTypeName }; - // token data.token = store.state.token || ''; - data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid; // 从缓存中获取uniacid,或使用默认uniacid,支持快应用 + data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid; - // 门店id var default_store_info = store.state.defaultStoreInfo; if (default_store_info) { data.store_id = default_store_info.store_id; @@ -75,122 +176,34 @@ export default { if (store_info) data.store_id = store_info.store_id; - // 参数 if (params.data != undefined) Object.assign(data, params.data); + const header = params.header || { + 'content-type': 'application/x-www-form-urlencoded;application/json' + }; + const dataType = params.dataType || 'json'; + const responseType = params.responseType || 'text'; + if (params.async === false) { - //同步 - return new Promise((resolve, reject) => { - uni.request({ - url: url, - method: method, - data: data, - header: params.header || { - // 'Accept': 'application/json', - 'content-type': 'application/x-www-form-urlencoded;application/json' - }, - dataType: params.dataType || 'json', - responseType: params.responseType || 'text', - success: (res) => { - // try { - // res.data = JSON.parse(res.data); - // } catch (e) { - // //TODO handle the exception - // console.log('api error:', e); - // } - if (res.data.code == -3 && store.state.siteState > 0) { - store.commit('setSiteState', -3); - Util.redirectTo('/pages_tool/storeclose/storeclose', {}, 'reLaunch'); - return; - } - if (res.data.refreshtoken) { - store.commit('setToken', res.data.refreshtoken); - } - if (res.data.code == -10009 || res.data.code == -10010) { - store.commit('setToken', ''); - store.commit('setMemberInfo', ''); - } - resolve(res.data); - }, - fail: (res) => { - if (res.errMsg && res.errMsg == 'request:fail url not in domain list') { - uni.showToast({ - title: getLang().lang('http.domainNotInList', { url: Config.baseUrl }), - 'icon': 'none', - duration: 10000 - }); - return; - } - reject(res); - }, - complete: (res) => { - // success/fail 已处理 resolve/reject,complete 只负责兜底提示非预期状态 - if ((res.errMsg && res.errMsg != "request:ok") || (res.statusCode && [200, 500].indexOf(res.statusCode) == -1)) { - uni.showToast({ - title: getLang().lang('http.requestFailed', { url: Config.baseUrl }), - 'icon': 'none', - duration: 10000 - }); - } - } + return _requestWithRetry(params, url, method, data, header, dataType, responseType) + .then(res => { + typeof params.complete == 'function' && params.complete(res); + return res; + }) + .catch(err => { + typeof params.complete == 'function' && params.complete(); + throw err; }); - }); } else { - //异步 - uni.request({ - url: url, - method: method, - data: data, - header: params.header || { - // 'Accept': 'application/json', - 'content-type': 'application/x-www-form-urlencoded;application/json' - }, - dataType: params.dataType || 'json', - responseType: params.responseType || 'text', - success: (res) => { - // try { - // res.data = JSON.parse(res.data); - // } catch (e) { - // //TODO handle the exception - // console.log('api error:', e); - // } - if (res.data.code == -3 && store.state.siteState > 0) { - store.commit('setSiteState', -3); - Util.redirectTo('/pages_tool/storeclose/storeclose', {}, 'reLaunch'); - return; - } - if (res.data.refreshtoken) { - store.commit('setToken', res.data.refreshtoken); - } - if (res.data.code == -10009 || res.data.code == -10010) { - store.commit('setToken', ''); - store.commit('setMemberInfo', ''); - } - typeof params.success == 'function' && params.success(res.data); - }, - fail: (res) => { - if (res.errMsg && res.errMsg == 'request:fail url not in domain list') { - uni.showToast({ - title: getLang().lang('http.domainNotInList', { url: Config.baseUrl, err: res.errMsg }), - 'icon': 'none', - duration: 10000 - }); - return; - } - typeof params.fail == 'function' && params.fail(res); - }, - complete: (res) => { - if ((res.errMsg && res.errMsg != "request:ok") || (res.statusCode && [200, 500].indexOf(res.statusCode) == -1)) { - uni.showToast({ - title: getLang().lang('http.requestFailed', { url: Config.baseUrl, err: res.errMsg }), - 'icon': 'none', - duration: 10000 - }); - return; - } - typeof params.complete == 'function' && params.complete(res.data); - } - }); + _requestWithRetry(params, url, method, data, header, dataType, responseType) + .then(res => { + typeof params.success == 'function' && params.success(res); + typeof params.complete == 'function' && params.complete(res); + }) + .catch(err => { + typeof params.fail == 'function' && params.fail(err); + typeof params.complete == 'function' && params.complete(); + }); } }, uploadBase64(params) { @@ -258,4 +271,4 @@ export default { } }); } -} +} \ No newline at end of file diff --git a/components/ns-payment/ns-payment.vue b/components/ns-payment/ns-payment.vue index d34ce7c..2766da8 100644 --- a/components/ns-payment/ns-payment.vue +++ b/components/ns-payment/ns-payment.vue @@ -231,6 +231,7 @@ export default { } this.$api.sendRequest({ url: '/api/pay/pay', + noRetry: true, data: { out_trade_no: this.payInfo.out_trade_no, pay_type: payType.type, @@ -350,6 +351,7 @@ export default { if (!payType) return; this.$api.sendRequest({ url: '/api/pay/pay', + noRetry: true, data: { out_trade_no: this.payInfo.out_trade_no, pay_type: payType.type, diff --git a/lang/en-us/common.js b/lang/en-us/common.js index e586268..7f07cb3 100644 --- a/lang/en-us/common.js +++ b/lang/en-us/common.js @@ -132,6 +132,16 @@ export const lang = { domainNotInList: '{url} is not in the request domain whitelist', // request failed: {url} is the domain placeholder requestFailed: '{url} request failed. {err}', + // network timeout + timeout: 'Network request timeout, please try again later', + // network connection failed + networkError: 'Network connection failed, please check your network', + // server error + serverError: 'Service temporarily unavailable, please try again later', + // request aborted + aborted: 'Request cancelled', + // unknown error + unknownError: 'Request failed, please try again later', }, // share utility diff --git a/lang/zh-cn/common.js b/lang/zh-cn/common.js index dc563fa..31e6665 100644 --- a/lang/zh-cn/common.js +++ b/lang/zh-cn/common.js @@ -132,6 +132,16 @@ export const lang = { domainNotInList: '{url}不在request 合法域名列表中', // 请求失败: {url} 为域名占位符 requestFailed: '{url}请求失败. {err}', + // 网络超时 + timeout: '网络请求超时,请稍后重试', + // 网络连接失败 + networkError: '网络连接失败,请检查网络设置', + // 服务器错误 + serverError: '服务暂时不可用,请稍后重试', + // 请求被中断 + aborted: '请求已取消', + // 未知错误 + unknownError: '请求失败,请稍后重试', }, // share 分享工具 diff --git a/pages/index/index.vue b/pages/index/index.vue index 7223fb6..14adb33 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -21,28 +21,28 @@ --> - - - - - - - - - {{ versionWatermark }} - - + :scrollTop="scrollTop" :diyGlobal="diyData.global" class="diy-index-page"> + + + + + + + + + {{ versionWatermark }} + + +