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 }}
+
+
+
diff --git a/pages_goods/category.vue b/pages_goods/category.vue
index ff370db..dd813d7 100644
--- a/pages_goods/category.vue
+++ b/pages_goods/category.vue
@@ -1,12 +1,12 @@
-
+
-
+
@@ -26,7 +26,7 @@
export default {
data() {
return {
- diyData: null,
+ diyData: { value: [] },
mpShareData: null //小程序分享数据
};
},
@@ -38,8 +38,15 @@ export default {
},
onShow() {
this.$util.hideHomeButton();
- if (this.$refs.category) this.$refs.category[0].pageShow();
-
+ try {
+ // 鸿蒙元服务 JSVM 下 $refs.category 可能为 undefined, 需防御
+ if (this.$refs.category && this.$refs.category[0]) {
+ this.$refs.category[0].pageShow();
+ }
+ } catch (e) {
+ console.warn('onShow pageShow failed', e);
+ }
+
//小程序分享
// #ifdef MP-WEIXIN
this.$util.getMpShare('/pages_goods/category').then(res => {
@@ -60,25 +67,48 @@ export default {
// #endif
},
onUnload() {
- !this.storeToken && this.$refs.login?.cancelCompleteInfo?.();
+ try {
+ if (!this.storeToken && this.$refs.login && typeof this.$refs.login.cancelCompleteInfo === 'function') {
+ this.$refs.login.cancelCompleteInfo();
+ }
+ } catch (e) {
+ console.warn('onUnload cancelCompleteInfo failed', e);
+ }
},
methods: {
getDiyInfo() {
+ // 避免重复请求导致布局抖动
+ if (this._diyInfoLoading) return;
+ this._diyInfoLoading = true;
+
this.$api.sendRequest({
url: '/api/diyview/info',
data: {
name: 'DIY_VIEW_GOODS_CATEGORY'
},
success: res => {
- if (res.code == 0 && res.data) {
- this.diyData = res.data;
- if (this.diyData.value) {
- this.diyData = JSON.parse(this.diyData.value);
- if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
+ if (res.code == 0 && res.data) {
+ const newData = res.data.value ? JSON.parse(res.data.value) : res.data;
+ // 仅在数据真正变化时赋值,避免鸿蒙元服务下频繁 re-render 造成频闪
+ if (JSON.stringify(newData) !== JSON.stringify(this.diyData)) {
+ this.diyData = newData;
}
-
- uni.stopPullDownRefresh();
+ } else {
+ this.$util.showToast({
+ title: this.$lang('diy.noDiyData')
+ });
}
+ },
+ fail: err => {
+ console.error('getDiyInfo request failed', err);
+ this.$util.showToast({
+ title: this.$lang('diy.networkError')
+ });
+ },
+ complete: () => {
+ this._diyInfoLoading = false;
+ if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
+ uni.stopPullDownRefresh();
}
});
},
diff --git a/pages_order/public/js/orderMethod.js b/pages_order/public/js/orderMethod.js
index 02f6e26..078996e 100644
--- a/pages_order/public/js/orderMethod.js
+++ b/pages_order/public/js/orderMethod.js
@@ -22,6 +22,7 @@ export default {
pay() {
this.$api.sendRequest({
url: '/api/order/pay',
+ noRetry: true,
data: {
order_ids: this.orderData.order_id
},
@@ -49,6 +50,7 @@ export default {
if (res.confirm) {
this.$api.sendRequest({
url: '/api/order/close',
+ noRetry: true,
data: {
order_id
},
@@ -82,6 +84,7 @@ export default {
if (res.confirm) {
this.$api.sendRequest({
url: '/api/order/takedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -110,6 +113,7 @@ export default {
if (res.extraData.status == 'success') {
this.$api.sendRequest({
url: '/api/order/takedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -123,6 +127,7 @@ export default {
} else {
this.$api.sendRequest({
url: '/api/order/takedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -146,6 +151,7 @@ export default {
if (res.confirm) {
this.$api.sendRequest({
url: '/api/order/takedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -177,6 +183,7 @@ export default {
if (res.confirm) {
this.$api.sendRequest({
url: '/api/order/membervirtualtakedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -205,6 +212,7 @@ export default {
if (res.extraData.status == 'success') {
this.$api.sendRequest({
url: '/api/order/membervirtualtakedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -218,6 +226,7 @@ export default {
} else {
this.$api.sendRequest({
url: '/api/order/membervirtualtakedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},
@@ -240,6 +249,7 @@ export default {
if (res.confirm) {
this.$api.sendRequest({
url: '/api/order/membervirtualtakedelivery',
+ noRetry: true,
data: {
order_id: orderData.order_id
},