refactor(http,lang): 重构请求逻辑并新增多语言错误提示

1.  新增网络相关错误的多语言翻译配置
2.  重写http请求层,添加自动重试机制与错误分类提示
3.  优化鸿蒙平台下的组件渲染兼容性与数据更新逻辑
4.  调整部分页面的模板结构与请求配置
This commit is contained in:
2026-07-13 15:45:15 +08:00
parent cb9234bdac
commit 30cda71749
8 changed files with 234 additions and 157 deletions

View File

@@ -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;
}

View File

@@ -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 {
}
});
}
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -132,6 +132,16 @@ export const lang = {
domainNotInList: '{url}不在request 合法域名列表中',
// 请求失败: {url} 为域名占位符
requestFailed: '{url}请求失败. {err}',
// 网络超时
timeout: '网络请求超时,请稍后重试',
// 网络连接失败
networkError: '网络连接失败,请检查网络设置',
// 服务器错误
serverError: '服务暂时不可用,请稍后重试',
// 请求被中断
aborted: '请求已取消',
// 未知错误
unknownError: '请求失败,请稍后重试',
},
// share 分享工具

View File

@@ -21,28 +21,28 @@
</view> -->
<diy-index-page v-if="topIndexValue" ref="indexPage" :value="topIndexValue" :bgUrl="bgUrl"
:scrollTop="scrollTop" :diyGlobal="diyData.global" class="diy-index-page">
<template v-slot:components>
<diy-group ref="diyGroup" v-if="diyData.value" :diyData="diyData" :scrollTop="scrollTop"
:haveTopCategory="true" :followOfficialAccount="followOfficialAccount"
@loaded="onDiyGroupLoaded" />
</template>
<template v-slot:default>
<ns-copyright v-show="isShowCopyRight" />
</template>
</diy-index-page>
<view v-else class="bg-index"
:style="{ backgroundImage: backgroundUrl, paddingTop: paddingTop, marginTop: marginTop }">
<diy-group ref="diyGroup" v-if="diyData.value" :diyData="diyData" :scrollTop="scrollTop"
:followOfficialAccount="followOfficialAccount" @loaded="onDiyGroupLoaded" />
<view class="version-watermark-wrap" v-if="versionWatermark && diyGroupReady" @tap="onVersionTap">
<view class="version-watermark">
<text>{{ versionWatermark }}</text>
</view>
</view>
:scrollTop="scrollTop" :diyGlobal="diyData.global" class="diy-index-page">
<template v-slot:components>
<diy-group v-show="diyData.value" ref="diyGroup" :diyData="diyData" :scrollTop="scrollTop"
:haveTopCategory="true" :followOfficialAccount="followOfficialAccount"
@loaded="onDiyGroupLoaded" />
</template>
<template v-slot:default>
<ns-copyright v-show="isShowCopyRight" />
</template>
</diy-index-page>
<view v-else class="bg-index"
:style="{ backgroundImage: backgroundUrl, paddingTop: paddingTop, marginTop: marginTop }">
<diy-group v-show="diyData.value" ref="diyGroup" :diyData="diyData" :scrollTop="scrollTop"
:followOfficialAccount="followOfficialAccount" @loaded="onDiyGroupLoaded" />
<view class="version-watermark-wrap" v-if="versionWatermark && diyGroupReady" @tap="onVersionTap">
<view class="version-watermark">
<text>{{ versionWatermark }}</text>
</view>
</view>
<ns-copyright v-show="isShowCopyRight" />
</view>
<template v-if="adv.advshow != -1">
<view @touchmove.prevent.stop>

View File

@@ -1,12 +1,12 @@
<template>
<view :style="themeColor">
<block v-if="diyData">
<view v-show="diyData && diyData.value && diyData.value.length">
<block v-for="(item, index) in diyData.value" :key="index">
<view v-if="item.componentName == 'GoodsCategory'">
<diy-category @tologin="toLogin" ref="category" :value="item"></diy-category>
</view>
</block>
</block>
</view>
<ns-login ref="login"></ns-login>
@@ -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();
}
});
},

View File

@@ -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
},