feat(diy): add in-page error prompt for diy data loading failure
1. add multi-language error copy for api request and network exceptions 2. add fixed top error banner component for index page 3. rewrite diy data request error handling: replace toast with reliable in-page prompt with retry button 4. add detailed error reason and request url display in error prompt
This commit is contained in:
@@ -51,6 +51,11 @@ export default {
|
|||||||
lastDiyInfoTime: 0, // 上次成功加载的时间戳
|
lastDiyInfoTime: 0, // 上次成功加载的时间戳
|
||||||
lastDiyInfoLang: '', // 上次加载时的语言,语言切换时强制刷新
|
lastDiyInfoLang: '', // 上次加载时的语言,语言切换时强制刷新
|
||||||
diyInfoTtl: 3 * 60 * 1000, // 3分钟内不重复请求
|
diyInfoTtl: 3 * 60 * 1000, // 3分钟内不重复请求
|
||||||
|
|
||||||
|
// diy 数据网络加载失败时的页面内错误提示(比 showModal 在各端更可靠)
|
||||||
|
diyLoadError: false,
|
||||||
|
diyLoadErrorMsg: '',
|
||||||
|
diyLoadErrorUrl: '', // 出错的网络请求地址
|
||||||
|
|
||||||
//启动广告
|
//启动广告
|
||||||
adv:{},
|
adv:{},
|
||||||
@@ -310,6 +315,52 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
// 无法从网络获取 diy 数据时:在页面内渲染错误横幅(各端可靠、一定可见)
|
||||||
|
// @param {Object|string} [err] 网络异常对象 {errMsg, statusCode} / 服务端消息字符串
|
||||||
|
// @param {string} [url] 出错的请求地址(完整 URL),用于横幅展示
|
||||||
|
_showDiyLoadError(err, url) {
|
||||||
|
this.diyLoadErrorMsg = this._buildDiyErrorMsg(err);
|
||||||
|
this.diyLoadErrorUrl = url || '';
|
||||||
|
this.diyLoadError = true;
|
||||||
|
},
|
||||||
|
// 根据错误信息组织更详细的提示文案
|
||||||
|
// 说明:remote-only 模式下 $lang 可能返回空串,故统一用 `|| 中文兜底` 保证非空
|
||||||
|
_buildDiyErrorMsg(err) {
|
||||||
|
// 无错误信息 -> 通用网络异常
|
||||||
|
if (!err) return this.$lang('diy.networkError');
|
||||||
|
// 服务端明确返回的业务消息(字符串)
|
||||||
|
if (typeof err === 'string') return err;
|
||||||
|
|
||||||
|
const statusCode = err.statusCode;
|
||||||
|
const errMsg = err.errMsg || err.message || '';
|
||||||
|
let reason;
|
||||||
|
|
||||||
|
if (statusCode) {
|
||||||
|
// HTTP 状态码类错误
|
||||||
|
if (statusCode === 404) reason = this.$lang('diy.errNotFound');
|
||||||
|
else if (statusCode >= 500) reason = this.$lang('diy.errServer');
|
||||||
|
else if (statusCode === 401 || statusCode === 403) reason = this.$lang('diy.errAuth');
|
||||||
|
else reason = this.$lang('diy.errRequest');
|
||||||
|
reason += '(' + statusCode + ')';
|
||||||
|
} else if (/timeout/i.test(errMsg)) {
|
||||||
|
reason = this.$lang('diy.errTimeout');
|
||||||
|
} else if (/(connect|network|fail)/i.test(errMsg)) {
|
||||||
|
reason = this.$lang('diy.errNetwork');
|
||||||
|
} else {
|
||||||
|
// 未知错误 -> 通用网络异常
|
||||||
|
return this.$lang('diy.networkError');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tpl = this.$lang('diy.loadFailTpl');
|
||||||
|
return tpl.replace('{reason}', reason);
|
||||||
|
},
|
||||||
|
// 错误横幅「重试」入口
|
||||||
|
retryDiyInfo() {
|
||||||
|
this.diyLoadError = false;
|
||||||
|
this.diyLoadErrorMsg = '';
|
||||||
|
this.diyLoadErrorUrl = '';
|
||||||
|
this.getDiyInfo(true);
|
||||||
|
},
|
||||||
async getDiyInfo(force = false) {
|
async getDiyInfo(force = false) {
|
||||||
// 节流:正在请求 / N 秒内已加载过 / 语言未变 -> 跳过
|
// 节流:正在请求 / N 秒内已加载过 / 语言未变 -> 跳过
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -324,9 +375,12 @@ export default {
|
|||||||
|
|
||||||
this.diyInfoLoading = true;
|
this.diyInfoLoading = true;
|
||||||
let res;
|
let res;
|
||||||
|
// 出错时用于展示的完整请求地址(Request URL),与 http.js 拼接/归一化方式保持一致
|
||||||
|
const diyInfoUrl = '/api/diyview/info';
|
||||||
|
const diyInfoFullUrl = ((Config.baseUrl || '') + diyInfoUrl).replace(/(?<!:)\/+/g, '/');
|
||||||
try {
|
try {
|
||||||
res = await this.$api.sendRequest({
|
res = await this.$api.sendRequest({
|
||||||
url: '/api/diyview/info',
|
url: diyInfoUrl,
|
||||||
data: {
|
data: {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -337,10 +391,9 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getDiyInfo request failed', e);
|
console.error('getDiyInfo request failed', e);
|
||||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||||
this.$util.showToast({
|
|
||||||
title: this.$lang('diy.networkError')
|
|
||||||
});
|
|
||||||
this.diyInfoLoading = false;
|
this.diyInfoLoading = false;
|
||||||
|
// 无法从网络获取数据时,页面内显示错误横幅并提供重试(带详细原因 + 请求地址)
|
||||||
|
this._showDiyLoadError(e, diyInfoFullUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.diyInfoLoading = false;
|
this.diyInfoLoading = false;
|
||||||
@@ -348,6 +401,14 @@ export default {
|
|||||||
if (!res || res.code != 0 || !res.data) {
|
if (!res || res.code != 0 || !res.data) {
|
||||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||||
|
|
||||||
|
// 完全没有拿到网络响应,视为无法从网络获取数据,页面内显示错误横幅
|
||||||
|
if (!res) {
|
||||||
|
this._showDiyLoadError(null, diyInfoFullUrl);
|
||||||
|
// 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局
|
||||||
|
this.diyData = { ...(this.diyData || {}), value: [] };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (res && res.code == -3) {
|
if (res && res.code == -3) {
|
||||||
this.$util.showToast({
|
this.$util.showToast({
|
||||||
title: res.message
|
title: res.message
|
||||||
@@ -357,9 +418,9 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$util.showToast({
|
// 拿到响应但业务失败 / 无数据(如接口 404 返回 200+错误码、code!=0)
|
||||||
title: this.$lang('diy.noDiyData')
|
// 同样视为无法获取到 diy 数据,页面内显示错误横幅
|
||||||
});
|
this._showDiyLoadError(res && res.message, diyInfoFullUrl);
|
||||||
// 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局
|
// 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局
|
||||||
this.diyData = { ...(this.diyData || {}), value: [] };
|
this.diyData = { ...(this.diyData || {}), value: [] };
|
||||||
return;
|
return;
|
||||||
@@ -367,6 +428,10 @@ export default {
|
|||||||
|
|
||||||
this.lastDiyInfoTime = now;
|
this.lastDiyInfoTime = now;
|
||||||
this.lastDiyInfoLang = currentLang;
|
this.lastDiyInfoLang = currentLang;
|
||||||
|
// 成功加载后清除错误提示
|
||||||
|
this.diyLoadError = false;
|
||||||
|
this.diyLoadErrorMsg = '';
|
||||||
|
this.diyLoadErrorUrl = '';
|
||||||
|
|
||||||
let diyDataValue = res.data;
|
let diyDataValue = res.data;
|
||||||
if (diyDataValue.value) {
|
if (diyDataValue.value) {
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ export const lang = {
|
|||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
noDiyData: 'Custom page data not configured',
|
noDiyData: 'Custom page data not configured',
|
||||||
locationNotAvailable: 'Location not available',
|
locationNotAvailable: 'Location not available',
|
||||||
|
// detailed data-load failure messages
|
||||||
|
loadFailTpl: 'Failed to load page data: {reason}, please try again later',
|
||||||
|
errNotFound: 'API not found',
|
||||||
|
errServer: 'Server error',
|
||||||
|
errAuth: 'No access permission',
|
||||||
|
errRequest: 'Request failed',
|
||||||
|
errTimeout: 'Request timeout',
|
||||||
|
errNetwork: 'Network connection failed',
|
||||||
},
|
},
|
||||||
|
|
||||||
//customer service
|
//customer service
|
||||||
@@ -142,6 +150,8 @@ export const lang = {
|
|||||||
serverError: 'Service temporarily unavailable, please try again later',
|
serverError: 'Service temporarily unavailable, please try again later',
|
||||||
// request aborted
|
// request aborted
|
||||||
aborted: 'Request cancelled',
|
aborted: 'Request cancelled',
|
||||||
|
// unknown error (errorType fallback, e.g. non-5xx HTTP status like 404)
|
||||||
|
unknown: 'Request failed, please try again later',
|
||||||
// unknown error
|
// unknown error
|
||||||
unknownError: 'Request failed, please try again later',
|
unknownError: 'Request failed, please try again later',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ export const lang = {
|
|||||||
retry: '重试',
|
retry: '重试',
|
||||||
noDiyData: '未配置自定义页面数据',
|
noDiyData: '未配置自定义页面数据',
|
||||||
locationNotAvailable: '未获取到定位',
|
locationNotAvailable: '未获取到定位',
|
||||||
|
// 数据加载失败详细文案
|
||||||
|
loadFailTpl: '页面数据加载失败:{reason},请稍后重试',
|
||||||
|
errNotFound: '接口不存在',
|
||||||
|
errServer: '服务器异常',
|
||||||
|
errAuth: '无访问权限',
|
||||||
|
errRequest: '请求失败',
|
||||||
|
errTimeout: '请求超时',
|
||||||
|
errNetwork: '网络连接失败',
|
||||||
},
|
},
|
||||||
|
|
||||||
// 客服服务
|
// 客服服务
|
||||||
@@ -142,6 +150,8 @@ export const lang = {
|
|||||||
serverError: '服务暂时不可用,请稍后重试',
|
serverError: '服务暂时不可用,请稍后重试',
|
||||||
// 请求被中断
|
// 请求被中断
|
||||||
aborted: '请求已取消',
|
aborted: '请求已取消',
|
||||||
|
// 未知错误(errorType 分类兜底,如非 5xx 的 HTTP 状态码 404 等)
|
||||||
|
unknown: '请求失败,请稍后重试',
|
||||||
// 未知错误
|
// 未知错误
|
||||||
unknownError: '请求失败,请稍后重试',
|
unknownError: '请求失败,请稍后重试',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
<view :style="themeColor">
|
<view :style="themeColor">
|
||||||
<view :style="{ backgroundColor: bgColor, minHeight: openBottomNav ? 'calc(100vh - 55px)' : '' }"
|
<view :style="{ backgroundColor: bgColor, minHeight: openBottomNav ? 'calc(100vh - 55px)' : '' }"
|
||||||
class="page-img">
|
class="page-img">
|
||||||
|
<!-- diy 数据网络加载失败提示(页面内横幅,各端可靠,悬浮在最上层) -->
|
||||||
|
<view v-if="diyLoadError" class="diy-load-error">
|
||||||
|
<view class="diy-load-error__body">
|
||||||
|
<text class="diy-load-error__msg">{{ diyLoadErrorMsg || '网络异常,请稍后重试' }}</text>
|
||||||
|
<text v-if="diyLoadErrorUrl" class="diy-load-error__url" selectable user-select>{{ diyLoadErrorUrl }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="diy-load-error__btn" @tap="retryDiyInfo">{{ $lang('diy.retry') || '重试' }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="site-info-box"
|
<view class="site-info-box"
|
||||||
v-if="$util.isWeiXin() && followOfficialAccount && followOfficialAccount.isShow && wechatQrcode">
|
v-if="$util.isWeiXin() && followOfficialAccount && followOfficialAccount.isShow && wechatQrcode">
|
||||||
<view class="site-info">
|
<view class="site-info">
|
||||||
@@ -179,6 +188,52 @@ export default {
|
|||||||
z-index: 122;
|
z-index: 122;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diy-load-error {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
padding-top: calc(24rpx + var(--status-bar-height, 0px));
|
||||||
|
background-color: #fff5f5;
|
||||||
|
border-bottom: 1rpx solid #ffccc7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diy-load-error__body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diy-load-error__msg {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #cf1322;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diy-load-error__url {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diy-load-error__btn {
|
||||||
|
margin-left: 20rpx;
|
||||||
|
padding: 10rpx 28rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #cf1322;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.small-bot swiper {
|
.small-bot swiper {
|
||||||
width: 560rpx;
|
width: 560rpx;
|
||||||
height: 784rpx;
|
height: 784rpx;
|
||||||
|
|||||||
Reference in New Issue
Block a user