From 8597504b574649b3f6343eb1fd84b859afc7af2a Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 09:15:26 +0800 Subject: [PATCH 01/46] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0.trae=E5=88=B0?= =?UTF-8?q?gitignore=E5=BF=BD=E7=95=A5=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将.trae目录加入gitignore,避免将本地临时目录提交到版本库 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f98040d..d619add 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .codebuddy .vscode +.trae # 鸿蒙元服务构建产物 stub-app-projects/mp-harmony/ascf/ascf_src From aa155189808e354e14a0638c784411128f8573ba Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 09:59:18 +0800 Subject: [PATCH 02/46] feat(splash-screen): add configurable splash screen display options add splashConfig in config-external.js to control progress/label/percent display, add runtime config api in splash-state.js, refactor splash-screen.vue to support inline layout and prop override --- common/js/config-external.js | 15 ++ common/js/splash-state.js | 33 +++++ components/splash-screen/splash-screen.vue | 155 ++++++++++++++++----- 3 files changed, 165 insertions(+), 38 deletions(-) diff --git a/common/js/config-external.js b/common/js/config-external.js index b380f61..eae8989 100644 --- a/common/js/config-external.js +++ b/common/js/config-external.js @@ -158,6 +158,21 @@ export const privacyConfig = { // }, }; +// ============================================================ +// 启动屏配置 (与 privacyConfig 同构, 由 splash-state.js 消费) +// showProgress 是否显示进度条本身 + 数字 (label 单独可独立关闭) +// showLabel 是否显示阶段文案 (Loading language... 等) +// showPercent 是否显示百分比数字 +// showProgress=true + showLabel=false + showPercent=true → 只显示进度条+百分比 +// showProgress=false → 整个进度条区域隐藏 +// 业务侧可改 splashConfig.* 或在 splash 组件 覆盖 +// ============================================================ +export const splashConfig = { + showProgress: true, + showLabel: false, // 默认 false: 用户想要"只显示百分比" + showPercent: true, +} + // 配置外置管理类 class ConfigExternal { constructor() { diff --git a/common/js/splash-state.js b/common/js/splash-state.js index 5f66b6f..5e2e363 100644 --- a/common/js/splash-state.js +++ b/common/js/splash-state.js @@ -10,8 +10,12 @@ // 4. 兜底超时: 默认 8s 未关闭则强制 hide,防止卡死 // 5. 平台隔离: APP-PLUS 有原生 splash,这里直接返回 false // 6. 跨平台兼容: 同步状态可被 Vue 组件直接 watch +// 7. 可配置: splashConfig 支持 showProgress/showLabel/showPercent 单独控制 +// 业务侧可在 config-external.js 改 splashConfig, 或 覆盖 // ============================================================ +import { splashConfig as _defaultSplashConfig } from './config-external.js' + // APP-PLUS 直接返回 false (有原生启动屏) function needSplash() { // #ifdef APP-PLUS @@ -67,6 +71,8 @@ class SplashState { // 避免多通道并发时, splash 组件 re-render 风暴 this._pendingProgress = null; // 待 emit 的最新进度 this._flushTimer = null; // 节流定时器 + // 启动屏显示配置: 初始读 config-external.js 的 splashConfig, 可运行时改 + this._splashConfig = Object.assign({}, _defaultSplashConfig) } show(opts = {}) { @@ -343,6 +349,33 @@ getStageDef(stageId) { return STAGES[stageId] } +/** + * 读取启动屏显示配置 + * splash 组件读这个决定 label/percent 是否显示 + * 缺字段时回退到默认 (splashConfig=true) + * @returns {{showProgress: boolean, showLabel: boolean, showPercent: boolean}} + */ +getSplashConfig() { + const c = this._splashConfig || {} + return { + showProgress: c.showProgress !== false, + showLabel: !!c.showLabel, + showPercent: c.showPercent !== false, + } +} + + +/** + * 运行时合并更新 splash 配置 + * 全局 splashConfig 在 boot 前注入; 业务运行时改某些字段可用本方法 + * 不会触发组件 re-render (splash 组件用 props 优先) + * @param {Object} partial + */ +setSplashConfig(partial) { + if (!partial || typeof partial !== 'object') return + this._splashConfig = Object.assign({}, this._splashConfig, partial) +} + /** * 立即刷新挂起的进度更新 * - reportProgress 默认 16ms throttle (1 帧), 避免连续触发 splash 组件多次 re-render diff --git a/components/splash-screen/splash-screen.vue b/components/splash-screen/splash-screen.vue index 654795e..6b4bf72 100644 --- a/components/splash-screen/splash-screen.vue +++ b/components/splash-screen/splash-screen.vue @@ -23,14 +23,28 @@ - - - - + + + + + + + + {{ percent }}% - - {{ stageLabel }} - {{ percent }}% + + + + + + + {{ stageLabel }} + {{ percent }}% + @@ -42,6 +56,23 @@ export default { name: 'splash-screen', + // props 优先级: 页面级 > splashState.getSplashConfig() + // 任何 prop 为 undefined 时回退全局, 给业务侧最大灵活性 + // 注意: prop 命名前缀 'display' 避免与未来 splash-state 内部字段同名引起 Vue prop-mutation 误判 + props: { + hideProgress: { + type: Boolean, + default: undefined + }, + hideLabel: { + type: Boolean, + default: undefined + }, + hidePercent: { + type: Boolean, + default: undefined + }, + }, data() { return { // 默认 visible=true: 让 splash 组件一渲染就可见, 减少"白屏闪烁" @@ -51,16 +82,32 @@ leaving: false, percent: 0, stage: 'boot', - showProgress: false, + // 内部 showProgress 标志 (保留兼容, 由 created() 维护) + // 实际模板用 displayConfig.showProgress 决定, 这个内部标志走原逻辑 + _showProgressLegacy: false, }; }, computed: { // 直接从 splashState.STAGES 读取文案, 避免重复维护 STAGE_LABEL - // (splash-state.js 暴露 getStageDef() 避免外部直接读 STAGES) stageLabel() { const def = splashState.getStageDef(this.stage) return def ? def.label : this.stage }, + // 显示配置: props 优先 (hide 语义, false=show), 否则回退全局 splashConfig + // 注意: splashConfig 的 show* 是肯定语义 (true=show), 我们这里 hide* 取反 + displayConfig() { + const cfg = splashState.getSplashConfig() + return { + showProgress: this.hideProgress !== undefined ? !this.hideProgress : cfg.showProgress, + showLabel: this.hideLabel !== undefined ? !this.hideLabel : cfg.showLabel, + showPercent: this.hidePercent !== undefined ? !this.hidePercent : cfg.showPercent, + } + }, + // inline 布局: 只显示百分比 + 进度条 (label 隐藏) + // 此时进度条和百分比在同一水平线 (flex) + inlineMode() { + return this.displayConfig.showPercent && !this.displayConfig.showLabel + }, }, // created (而非 mounted): 提前同步 splashState 状态, 减少"白屏一帧" created() { @@ -68,7 +115,10 @@ const init = splashState.getProgress() this.percent = init.percent this.stage = init.stage - this.showProgress = init.percent > 0 + // _showProgressLegacy 控制 progress-fade-in CSS 动画触发 + // (true → 触发 CSS 关键帧; false → 隐藏整个进度区) + // 模板用 displayConfig.showProgress 综合 props + splashConfig 决定 + this._showProgressLegacy = init.percent > 0 } catch (e) {} // 同步 splashState 状态: 防止组件挂载在 splash 已 hide 之后误显示 if (!splashState.isShow()) { @@ -76,34 +126,34 @@ } }, mounted() { - splashState.notifyMounted(); - splashState.on('splash:show', () => { - this.visible = true; - this.leaving = false; - // 注意: 不要在这里再读 getProgress(). percent 不允许后退 - // 已通过 created() 同步; emit 进来多是无变化 - const init = splashState.getProgress() - if (init.percent > this.percent) this.percent = init.percent - if (init.stage !== this.stage) this.stage = init.stage - this.showProgress = true - }); - splashState.on('splash:hide', (info) => { - // 进度条补完到 100% (视觉上"完成"), 立即 leave - if (this.percent < 100) { - this.percent = 100; - this.stage = 'done'; - } - // mode='fast' (隐私弹窗即将出现) → 80ms 短动画让 modal 更快接管视觉 - const fast = info && info.mode === 'fast' - this._leave(fast) - }); - splashState.onProgress((info) => { - // 去重: percent/stage 没变化就不 setState, 避免空 re-render - if (info.percent !== this.percent) this.percent = info.percent - if (info.stage !== this.stage) this.stage = info.stage - this.showProgress = true; - }); - }, + splashState.notifyMounted(); + splashState.on('splash:show', () => { + this.visible = true; + this.leaving = false; + // 注意: 不要在这里再读 getProgress(). percent 不允许后退 + // 已通过 created() 同步; emit 进来多是无变化 + const init = splashState.getProgress() + if (init.percent > this.percent) this.percent = init.percent + if (init.stage !== this.stage) this.stage = init.stage + this._showProgressLegacy = true + }); + splashState.on('splash:hide', (info) => { + // 进度条补完到 100% (视觉上"完成"), 立即 leave + if (this.percent < 100) { + this.percent = 100; + this.stage = 'done'; + } + // mode='fast' (隐私弹窗即将出现) → 80ms 短动画让 modal 更快接管视觉 + const fast = info && info.mode === 'fast' + this._leave(fast) + }); + splashState.onProgress((info) => { + // 去重: percent/stage 没变化就不 setState, 避免空 re-render + if (info.percent !== this.percent) this.percent = info.percent + if (info.stage !== this.stage) this.stage = info.stage + this._showProgressLegacy = true; + }); + }, beforeDestroy() { splashState.off('splash:show'); splashState.off('splash:hide'); @@ -187,6 +237,18 @@ background: rgba($accent, 0.08); border-radius: 3rpx; overflow: hidden; + + // inline 模式 (showLabel=false, showPercent=true): 进度条占主宽, 比例调整 + &--inline { + flex: 1; + min-width: 0; + } + + // full 模式: 原始整条进度条 + &--full { + // 默认宽度 100% 由父容器 .splash-screen__progress 提供 + width: 100%; + } } .splash-screen__progress-fill { @@ -200,6 +262,15 @@ box-shadow: 0 0 12rpx rgba($accent, 0.4); } + // 新增: inline row 布局 (showLabel=false + showPercent=true) + // 进度条和百分比在同一水平线: 进度条 + 右侧百分比 + .splash-screen__progress-row { + display: flex; + align-items: center; + gap: 12rpx; + width: 100%; + } + .splash-screen__progress-text { display: flex; justify-content: space-between; @@ -220,6 +291,14 @@ font-variant-numeric: tabular-nums; font-weight: 500; color: $accent; + + // inline 模式: 字号略大, 与进度条同行 + &--inline { + flex-shrink: 0; + font-size: 24rpx; + min-width: 72rpx; + text-align: right; + } } @keyframes progress-fade-in { From 3f09dca054fa12bf54cfb932f946ab7d75c55ee3 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 10:22:45 +0800 Subject: [PATCH 03/46] =?UTF-8?q?refactor(splash-screen):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=BA=9F=E5=BC=83=E7=9A=84=E5=90=AF=E5=8A=A8=E5=B1=8F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 简化启动屏组件代码,删除不再使用的splashConfig全局配置、相关工具方法和冗余的布局逻辑,统一进度条展示样式。 --- common/js/config-external.js | 15 -- common/js/splash-state.js | 33 ---- components/splash-screen/splash-screen.vue | 194 ++------------------- 3 files changed, 18 insertions(+), 224 deletions(-) diff --git a/common/js/config-external.js b/common/js/config-external.js index eae8989..b380f61 100644 --- a/common/js/config-external.js +++ b/common/js/config-external.js @@ -158,21 +158,6 @@ export const privacyConfig = { // }, }; -// ============================================================ -// 启动屏配置 (与 privacyConfig 同构, 由 splash-state.js 消费) -// showProgress 是否显示进度条本身 + 数字 (label 单独可独立关闭) -// showLabel 是否显示阶段文案 (Loading language... 等) -// showPercent 是否显示百分比数字 -// showProgress=true + showLabel=false + showPercent=true → 只显示进度条+百分比 -// showProgress=false → 整个进度条区域隐藏 -// 业务侧可改 splashConfig.* 或在 splash 组件 覆盖 -// ============================================================ -export const splashConfig = { - showProgress: true, - showLabel: false, // 默认 false: 用户想要"只显示百分比" - showPercent: true, -} - // 配置外置管理类 class ConfigExternal { constructor() { diff --git a/common/js/splash-state.js b/common/js/splash-state.js index 5e2e363..5f66b6f 100644 --- a/common/js/splash-state.js +++ b/common/js/splash-state.js @@ -10,12 +10,8 @@ // 4. 兜底超时: 默认 8s 未关闭则强制 hide,防止卡死 // 5. 平台隔离: APP-PLUS 有原生 splash,这里直接返回 false // 6. 跨平台兼容: 同步状态可被 Vue 组件直接 watch -// 7. 可配置: splashConfig 支持 showProgress/showLabel/showPercent 单独控制 -// 业务侧可在 config-external.js 改 splashConfig, 或 覆盖 // ============================================================ -import { splashConfig as _defaultSplashConfig } from './config-external.js' - // APP-PLUS 直接返回 false (有原生启动屏) function needSplash() { // #ifdef APP-PLUS @@ -71,8 +67,6 @@ class SplashState { // 避免多通道并发时, splash 组件 re-render 风暴 this._pendingProgress = null; // 待 emit 的最新进度 this._flushTimer = null; // 节流定时器 - // 启动屏显示配置: 初始读 config-external.js 的 splashConfig, 可运行时改 - this._splashConfig = Object.assign({}, _defaultSplashConfig) } show(opts = {}) { @@ -349,33 +343,6 @@ getStageDef(stageId) { return STAGES[stageId] } -/** - * 读取启动屏显示配置 - * splash 组件读这个决定 label/percent 是否显示 - * 缺字段时回退到默认 (splashConfig=true) - * @returns {{showProgress: boolean, showLabel: boolean, showPercent: boolean}} - */ -getSplashConfig() { - const c = this._splashConfig || {} - return { - showProgress: c.showProgress !== false, - showLabel: !!c.showLabel, - showPercent: c.showPercent !== false, - } -} - - -/** - * 运行时合并更新 splash 配置 - * 全局 splashConfig 在 boot 前注入; 业务运行时改某些字段可用本方法 - * 不会触发组件 re-render (splash 组件用 props 优先) - * @param {Object} partial - */ -setSplashConfig(partial) { - if (!partial || typeof partial !== 'object') return - this._splashConfig = Object.assign({}, this._splashConfig, partial) -} - /** * 立即刷新挂起的进度更新 * - reportProgress 默认 16ms throttle (1 帧), 避免连续触发 splash 组件多次 re-render diff --git a/components/splash-screen/splash-screen.vue b/components/splash-screen/splash-screen.vue index 6b4bf72..d763a8f 100644 --- a/components/splash-screen/splash-screen.vue +++ b/components/splash-screen/splash-screen.vue @@ -2,10 +2,7 @@ - - - @@ -15,36 +12,17 @@ - - - - - - - - - - {{ percent }}% - - - - - - - - {{ stageLabel }} - {{ percent }}% - + + + {{ percent }}% + + @@ -56,71 +34,18 @@ export default { name: 'splash-screen', - // props 优先级: 页面级 > splashState.getSplashConfig() - // 任何 prop 为 undefined 时回退全局, 给业务侧最大灵活性 - // 注意: prop 命名前缀 'display' 避免与未来 splash-state 内部字段同名引起 Vue prop-mutation 误判 - props: { - hideProgress: { - type: Boolean, - default: undefined - }, - hideLabel: { - type: Boolean, - default: undefined - }, - hidePercent: { - type: Boolean, - default: undefined - }, - }, data() { return { - // 默认 visible=true: 让 splash 组件一渲染就可见, 减少"白屏闪烁" - // created() 钩子会同步 splashState 状态, 如果 splashState 已 hide, 立刻设 false - // 这样既能保证首屏冷启动可见, 又避免挂载到非 splash 路径时误显示 visible: true, leaving: false, percent: 0, - stage: 'boot', - // 内部 showProgress 标志 (保留兼容, 由 created() 维护) - // 实际模板用 displayConfig.showProgress 决定, 这个内部标志走原逻辑 - _showProgressLegacy: false, }; }, - computed: { - // 直接从 splashState.STAGES 读取文案, 避免重复维护 STAGE_LABEL - stageLabel() { - const def = splashState.getStageDef(this.stage) - return def ? def.label : this.stage - }, - // 显示配置: props 优先 (hide 语义, false=show), 否则回退全局 splashConfig - // 注意: splashConfig 的 show* 是肯定语义 (true=show), 我们这里 hide* 取反 - displayConfig() { - const cfg = splashState.getSplashConfig() - return { - showProgress: this.hideProgress !== undefined ? !this.hideProgress : cfg.showProgress, - showLabel: this.hideLabel !== undefined ? !this.hideLabel : cfg.showLabel, - showPercent: this.hidePercent !== undefined ? !this.hidePercent : cfg.showPercent, - } - }, - // inline 布局: 只显示百分比 + 进度条 (label 隐藏) - // 此时进度条和百分比在同一水平线 (flex) - inlineMode() { - return this.displayConfig.showPercent && !this.displayConfig.showLabel - }, - }, - // created (而非 mounted): 提前同步 splashState 状态, 减少"白屏一帧" created() { try { const init = splashState.getProgress() this.percent = init.percent - this.stage = init.stage - // _showProgressLegacy 控制 progress-fade-in CSS 动画触发 - // (true → 触发 CSS 关键帧; false → 隐藏整个进度区) - // 模板用 displayConfig.showProgress 综合 props + splashConfig 决定 - this._showProgressLegacy = init.percent > 0 } catch (e) {} - // 同步 splashState 状态: 防止组件挂载在 splash 已 hide 之后误显示 if (!splashState.isShow()) { this.visible = false } @@ -130,28 +55,18 @@ splashState.on('splash:show', () => { this.visible = true; this.leaving = false; - // 注意: 不要在这里再读 getProgress(). percent 不允许后退 - // 已通过 created() 同步; emit 进来多是无变化 const init = splashState.getProgress() if (init.percent > this.percent) this.percent = init.percent - if (init.stage !== this.stage) this.stage = init.stage - this._showProgressLegacy = true }); splashState.on('splash:hide', (info) => { - // 进度条补完到 100% (视觉上"完成"), 立即 leave if (this.percent < 100) { this.percent = 100; - this.stage = 'done'; } - // mode='fast' (隐私弹窗即将出现) → 80ms 短动画让 modal 更快接管视觉 const fast = info && info.mode === 'fast' this._leave(fast) }); splashState.onProgress((info) => { - // 去重: percent/stage 没变化就不 setState, 避免空 re-render if (info.percent !== this.percent) this.percent = info.percent - if (info.stage !== this.stage) this.stage = info.stage - this._showProgressLegacy = true; }); }, beforeDestroy() { @@ -165,12 +80,6 @@ } }, methods: { - /** - * 离开 splash - * - 默认: 480ms fade-out 动画 (视觉柔和过渡) - * - 当 privacy 弹窗即将出现时: 用 80ms 短动画, 让 modal 更快接管视觉 - * 否则弹窗与 splash 同时显示会有"双遮罩"感 - */ _leave(fast) { this.leaving = true; if (this._leaveTimer) clearTimeout(this._leaveTimer); @@ -185,15 +94,8 @@ + \ No newline at end of file From edea8c32de596f51b61243f3a5395c4a0f15aa90 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 10:36:04 +0800 Subject: [PATCH 04/46] =?UTF-8?q?fix(splash-screen):=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=B1=8F=E7=AD=89=E5=BE=85=E9=A6=96=E9=A1=B5=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E5=B0=B1=E7=BB=AA=E5=90=8E=E5=86=8D=E6=B6=88=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题描述: 首次启动时,启动屏已关闭但首页内容还是空白的,用户体验差。 解决方案: 1. 在 splash-state.js 中新增页面就绪等待机制 (_pageReady 标志、notifyPageReady 方法) 2. 修改软隐藏逻辑,必须同时满足组件已 mount 和页面已就绪才关闭启动屏 3. 在首页 onDiyGroupLoaded 回调中通知页面就绪 兜底保护: - 组件 1s 未 mount → 强制关闭 - 页面 2s 未就绪 → 强制关闭 - 总超时 8s → 强制关闭 --- common/js/splash-state.js | 60 ++++++++++++++++++++++++++++++---- pages/index/public/js/index.js | 4 +++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/common/js/splash-state.js b/common/js/splash-state.js index 5f66b6f..ebc6360 100644 --- a/common/js/splash-state.js +++ b/common/js/splash-state.js @@ -35,6 +35,7 @@ function useNativeLoading() { const DEFAULT_TIMEOUT = 8000; // 总兜底超时 const MOUNT_GRACE_MS = 1000; // 软隐藏后等组件 mount 的宽限时间 +const PAGE_READY_GRACE_MS = 2000; // 软隐藏后等页面就绪的宽限时间 // 启动期阶段定义 (id → { percent, label }) // percent: 阶段完成的百分比 (0-100) @@ -56,9 +57,12 @@ class SplashState { this._listeners = []; this._shownAt = 0; // 软隐藏相关 - this._pendingHide = null; // 软隐藏请求的 opts, 等待组件 mount - this._hasMounted = false; // 是否有 splash 组件 mount 过 - this._mountGraceId = null; // 软隐藏兜底定时器 + this._pendingHide = null; // 软隐藏请求的 opts, 等待组件 mount + this._hasMounted = false; // 是否有 splash 组件 mount 过 + this._mountGraceId = null; // 软隐藏兜底定时器 (等 mount) + // 页面就绪等待 + this._pageReady = false; // 页面内容是否已就绪 (由首页通知) + this._pageReadyGraceId = null; // 页面就绪兜底定时器 // 进度上报 this._stage = 'boot'; this._percent = 0; @@ -77,6 +81,7 @@ class SplashState { this._shownAt = Date.now(); this._pendingHide = null; this._hasMounted = false; + this._pageReady = false; this._stage = 'boot'; // 初始化时直接显示 'boot' 阶段的 5%, 而不是 0% // 避免首帧出现"进度条 0%"的视觉突兀 @@ -91,6 +96,10 @@ class SplashState { clearTimeout(this._mountGraceId); this._mountGraceId = null; } + if (this._pageReadyGraceId) { + clearTimeout(this._pageReadyGraceId); + this._pageReadyGraceId = null; + } this._emit('splash:show'); // H5 降级: 走系统 loading (Vue 还没挂载,组件层无法渲染) @@ -138,15 +147,16 @@ class SplashState { } /** - * 软隐藏: 标记应该 hide, 等组件 mount + * 软隐藏: 标记应该 hide, 等组件 mount + 页面就绪 * 如果组件 1s 内没 mount, 兜底自动 hide + * 如果页面 2s 内没就绪, 兜底自动 hide */ _softHide(opts) { if (!this._visible) return; // 已经隐藏了 this._pendingHide = opts; - // 如果组件已经 mount, 立即隐藏 - if (this._hasMounted) { + // 如果组件已经 mount 且页面已就绪, 立即隐藏 + if (this._hasMounted && this._pageReady) { this._flushPendingHide(); return; } @@ -162,6 +172,7 @@ class SplashState { /** * 真正触发软隐藏 + * 如果页面还未就绪, 等待页面就绪后再 hide */ _flushPendingHide() { const opts = this._pendingHide; @@ -170,6 +181,20 @@ class SplashState { clearTimeout(this._mountGraceId); this._mountGraceId = null; } + + // 如果页面还未就绪, 等待页面就绪 + if (!this._pageReady) { + // 兜底: 2s 内没就绪, 强制隐藏 + if (this._pageReadyGraceId) clearTimeout(this._pageReadyGraceId); + this._pageReadyGraceId = setTimeout(() => { + if (this._visible) { + console.warn('[splash-state] auto-hide after page-ready timeout'); + this._doHide(opts || { reason: 'page-ready-timeout' }); + } + }, PAGE_READY_GRACE_MS); + return; + } + this._doHide(opts || { reason: 'mount-completed' }); } @@ -186,6 +211,10 @@ class SplashState { clearTimeout(this._mountGraceId); this._mountGraceId = null; } + if (this._pageReadyGraceId) { + clearTimeout(this._pageReadyGraceId); + this._pageReadyGraceId = null; + } this._pendingHide = null; this._visible = false; // H5 降级对应的关闭 @@ -217,6 +246,25 @@ class SplashState { } } + /** + * 页面内容就绪后通知 state 模块 + * - 标记 _pageReady + * - 如果有 _pendingHide, 触发真关 + * 调用时机: 首页 diy-group 渲染完成后调用 + */ + notifyPageReady() { + if (this._pageReady) return; + this._pageReady = true; + if (this._pageReadyGraceId) { + clearTimeout(this._pageReadyGraceId); + this._pageReadyGraceId = null; + } + if (this._pendingHide) { + // 给页面一帧时间完成渲染, 再隐藏 + setTimeout(() => this._flushPendingHide(), 50); + } + } + /** * 组件 unmount 时通知 (用于 _hasMounted 状态管理) * 目前保留 _hasMounted=true, 因为一般只有 tab 页有 splash 组件, 不会动态增减 diff --git a/pages/index/public/js/index.js b/pages/index/public/js/index.js index 61041ba..3fef1eb 100644 --- a/pages/index/public/js/index.js +++ b/pages/index/public/js/index.js @@ -1,3 +1,5 @@ +import splashState from '@/common/js/splash-state.js'; + export default { data() { return { @@ -124,8 +126,10 @@ export default { return false; }, // diy-group 渲染完毕回调, 用于触发版本水印等延后显示元素 + // 同时通知 splash-state 页面就绪, 启动屏可以关闭 onDiyGroupLoaded() { this.diyGroupReady = true; + try { splashState.notifyPageReady(); } catch (e) {} }, /** * 版本连击 3 次以上显示构建版本信息(供测试人员使用) From 6b8b4acc96b3c8c6384a181f2156cff1a093d3bb Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 10:43:47 +0800 Subject: [PATCH 05/46] =?UTF-8?q?fix(splash-state):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=B0=B1=E7=BB=AA=E5=90=8E=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=90=AF=E5=8A=A8=E5=B1=8F=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题根源: _flushPendingHide() 中过早清除了 _pendingHide,导致页面就绪后 notifyPageReady() 检测不到 pending hide 请求,启动屏无法关闭。 修复方案: 当页面未就绪时,保留 _pendingHide 直到页面真正就绪后再清除。 --- common/js/splash-state.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/js/splash-state.js b/common/js/splash-state.js index ebc6360..10fb2aa 100644 --- a/common/js/splash-state.js +++ b/common/js/splash-state.js @@ -175,26 +175,26 @@ class SplashState { * 如果页面还未就绪, 等待页面就绪后再 hide */ _flushPendingHide() { - const opts = this._pendingHide; - this._pendingHide = null; if (this._mountGraceId) { clearTimeout(this._mountGraceId); this._mountGraceId = null; } - // 如果页面还未就绪, 等待页面就绪 + // 如果页面还未就绪, 等待页面就绪 (保留 _pendingHide, 等页面就绪后再处理) if (!this._pageReady) { // 兜底: 2s 内没就绪, 强制隐藏 if (this._pageReadyGraceId) clearTimeout(this._pageReadyGraceId); this._pageReadyGraceId = setTimeout(() => { - if (this._visible) { + if (this._visible && this._pendingHide) { console.warn('[splash-state] auto-hide after page-ready timeout'); - this._doHide(opts || { reason: 'page-ready-timeout' }); + this._doHide(this._pendingHide || { reason: 'page-ready-timeout' }); } }, PAGE_READY_GRACE_MS); return; } + const opts = this._pendingHide; + this._pendingHide = null; this._doHide(opts || { reason: 'mount-completed' }); } From 88921e61d22e29a0211e7722c49fa039f4750b64 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 10:54:17 +0800 Subject: [PATCH 06/46] =?UTF-8?q?fix(splash-screen):=20=E4=BF=AE=E5=A4=8DH?= =?UTF-8?q?5=E7=8E=AF=E5=A2=83=E4=B8=8B=E5=90=AF=E5=8A=A8=E5=B1=8F?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=90=8E=E9=A1=B5=E9=9D=A2=E7=A9=BA=E7=99=BD?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题根源分析: 1. diy-group 的 mounted() 在组件挂载时就触发了,但此时 diyData 还没有数据 2. otifyMounted() 在组件 mount 时立即关闭了 H5 的 uni.showLoading(),但页面内容还没就绪 3. 导致启动屏关闭后,首页内容还是空白的 修复方案: 1. 在 diy-group.vue 中,将 loaded 事件的触发时机改为 diyData watch 中,确保数据加载完成后再通知 2. 在 splash-state.js 中, otifyMounted() 不再立即关闭 H5 原生 loading,等待页面就绪后由 _doHide() 统一关闭 3. 在 index.js 中,onDiyGroupLoaded() 延迟 500ms 再通知页面就绪,确保内容渲染完成 --- common/js/splash-state.js | 7 +------ components-diy/diy-group.vue | 13 ++++++++++--- pages/index/public/js/index.js | 5 ++++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/js/splash-state.js b/common/js/splash-state.js index 10fb2aa..b34a6d9 100644 --- a/common/js/splash-state.js +++ b/common/js/splash-state.js @@ -231,17 +231,12 @@ class SplashState { * 组件 mount 后通知 state 模块 * - 标记 _hasMounted * - 如果有 _pendingHide, 触发真关 - * - H5 降级场景: 立即关掉原生 loading, 让 splash 组件接管视觉 + * - H5 降级场景: 不再立即关掉原生 loading, 等待页面就绪后再关 */ notifyMounted() { if (this._hasMounted) return; this._hasMounted = true; - // H5 降级: 立即关掉原生 loading, 避免和 splash 组件重叠 - if (useNativeLoading()) { - try { uni.hideLoading(); } catch (e) {} - } if (this._pendingHide) { - // 给组件一帧时间渲染, 再隐藏 (避免一帧闪烁) setTimeout(() => this._flushPendingHide(), 50); } } diff --git a/components-diy/diy-group.vue b/components-diy/diy-group.vue index 5794df5..edc192d 100644 --- a/components-diy/diy-group.vue +++ b/components-diy/diy-group.vue @@ -300,6 +300,7 @@ export default { handler() { this.diyGlobalData = this.diyData ? this.$util.deepClone(this.diyData) : null; this.processData(); + this._notifyLoaded(); }, deep: true } @@ -308,8 +309,9 @@ export default { this.processData(); }, mounted() { - // 通知父组件 diy-group 内容已渲染完毕 - this.$emit('loaded'); + if (this.diyData && this.diyData.value) { + this._notifyLoaded(); + } }, computed: { topNavColor() { @@ -323,7 +325,6 @@ export default { return topNavColor; }, - // 过滤组件的渲染 diyDataArray() { if (!this.processedData) { this.processData(); @@ -347,6 +348,12 @@ export default { } }, methods: { + _notifyLoaded() { + if (this._loadedTimer) clearTimeout(this._loadedTimer); + this._loadedTimer = setTimeout(() => { + this.$emit('loaded'); + }, 300); + }, processData() { if (!this.diyGlobalData || !this.diyGlobalData.value) { this.processedData = []; diff --git a/pages/index/public/js/index.js b/pages/index/public/js/index.js index 3fef1eb..b15c395 100644 --- a/pages/index/public/js/index.js +++ b/pages/index/public/js/index.js @@ -127,9 +127,12 @@ export default { }, // diy-group 渲染完毕回调, 用于触发版本水印等延后显示元素 // 同时通知 splash-state 页面就绪, 启动屏可以关闭 + // 延迟 500ms 是为了确保页面内容有足够时间渲染 onDiyGroupLoaded() { this.diyGroupReady = true; - try { splashState.notifyPageReady(); } catch (e) {} + setTimeout(() => { + try { splashState.notifyPageReady(); } catch (e) {} + }, 500); }, /** * 版本连击 3 次以上显示构建版本信息(供测试人员使用) From c739d691a2a2ef3d9e17426a45a8ca9d479cf211 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 14:11:14 +0800 Subject: [PATCH 07/46] =?UTF-8?q?fix(diy-video):=20=E9=80=82=E9=85=8D?= =?UTF-8?q?=E9=B8=BF=E8=92=99=E5=85=83=E6=9C=8D=E5=8A=A1=E7=AB=AF=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E7=BB=84=E4=BB=B6=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增鸿蒙平台专属video组件渲染分支 2. 新增窗口宽度初始化逻辑用于计算鸿蒙视频高度 3. 针对不同平台调整视频容器样式计算逻辑 4. 补充视频加载元数据和错误回调 5. 修复微信小程序视频比例适配逻辑 --- components-diy/diy-video.vue | 84 +++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/components-diy/diy-video.vue b/components-diy/diy-video.vue index 51a1cab..28a4012 100644 --- a/components-diy/diy-video.vue +++ b/components-diy/diy-video.vue @@ -26,6 +26,23 @@ :poster="posterUrl" :style="videoWarpCss" controls + object-fit="cover" + @loadedmetadata="onVideoLoaded" + @error="onVideoError" + > + + + + + @@ -45,13 +62,15 @@ export default { }, data() { return { - videoRatio: null, // 视频比例:'landscape'(横屏)或 'portrait'(竖屏) + videoRatio: null, videoUrl: '', - posterUrl: '' + posterUrl: '', + windowWidth: 0 }; }, created() { this.initVideoData(); + this.initWindowWidth(); }, mixins: [DiyMinx], watch: { @@ -79,18 +98,43 @@ export default { containerStyle() { let style = ''; + // #ifdef H5 if (this.videoRatio === 'landscape') { - // 横屏视频:16/9比例 style += 'aspect-ratio: 16 / 9;'; } else if (this.videoRatio === 'portrait') { - // 竖屏视频:9/16比例 style += 'aspect-ratio: 9 / 16;'; } else { - // 未加载完成 → 默认使用竖屏比例 style += 'aspect-ratio: 9 / 16;'; } + // #endif + + // #ifdef MP-WEIXIN + if (this.videoRatio === 'landscape') { + style += 'padding-top: 56.25%;'; + } else if (this.videoRatio === 'portrait') { + style += 'padding-top: 177.78%;'; + } else { + style += 'padding-top: 177.78%;'; + } + // #endif + + // #ifdef MP-HARMONY + style += 'width: 100%;'; + style += 'height: ' + this.harmonyVideoHeight + 'px;'; + // #endif return style; + }, + + // 鸿蒙平台视频高度 + harmonyVideoHeight() { + if (!this.videoRatio) { + return (this.windowWidth * 9) / 16; + } + if (this.videoRatio === 'landscape') { + return (this.windowWidth * 9) / 16; + } + return (this.windowWidth * 16) / 9; } }, methods: { @@ -104,6 +148,16 @@ export default { // #endif }, + // 初始化窗口宽度 + initWindowWidth() { + try { + const systemInfo = uni.getSystemInfoSync(); + this.windowWidth = systemInfo.windowWidth || 375; + } catch (e) { + this.windowWidth = 375; + } + }, + // 获取视频URL getVideoUrl() { if (!this.value || !this.value.videoUrl) return ''; @@ -169,6 +223,13 @@ export default { } // #endif + // #ifdef MP-HARMONY + if (event && event.detail) { + width = event.detail.width || 0; + height = event.detail.height || 0; + } + // #endif + if (width && height) { this.videoRatio = width >= height ? 'landscape' : 'portrait'; } @@ -225,6 +286,19 @@ export default { .mp-video { width: 100%; height: 100%; + position: absolute; + top: 0; + left: 0; +} +/* #endif */ + +/* 鸿蒙元服务端样式 */ +/* #ifdef MP-HARMONY */ +.harmony-video { + width: 100%; + height: 100%; + display: block; + box-sizing: border-box; } /* #endif */ From 10bc06f212dbc76c84dcf795a5f4bc41d63a6143 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 14:24:09 +0800 Subject: [PATCH 08/46] =?UTF-8?q?refactor(components/diy-video):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=A7=86=E9=A2=91=E6=AF=94=E4=BE=8B=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 替换原有的固定比例方案,改为通过视频实际宽高动态计算适配比例,移除冗余的harmonyVideoHeight计算方法,统一多端样式处理逻辑,优化鸿蒙端视频高度计算方式 --- components-diy/diy-video.vue | 61 +++++++++++++++--------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/components-diy/diy-video.vue b/components-diy/diy-video.vue index 28a4012..f770f7a 100644 --- a/components-diy/diy-video.vue +++ b/components-diy/diy-video.vue @@ -61,13 +61,14 @@ export default { } }, data() { - return { - videoRatio: null, - videoUrl: '', - posterUrl: '', - windowWidth: 0 - }; - }, + return { + videoWidth: 0, + videoHeight: 0, + videoUrl: '', + posterUrl: '', + windowWidth: 0 + }; + }, created() { this.initVideoData(); this.initWindowWidth(); @@ -97,44 +98,27 @@ export default { // 动态计算容器比例 containerStyle() { let style = ''; + const defaultRatio = 9 / 16; + + let ratio = defaultRatio; + if (this.videoWidth && this.videoHeight) { + ratio = this.videoHeight / this.videoWidth; + } // #ifdef H5 - if (this.videoRatio === 'landscape') { - style += 'aspect-ratio: 16 / 9;'; - } else if (this.videoRatio === 'portrait') { - style += 'aspect-ratio: 9 / 16;'; - } else { - style += 'aspect-ratio: 9 / 16;'; - } + style += 'padding-top: ' + (ratio * 100) + '%;'; // #endif // #ifdef MP-WEIXIN - if (this.videoRatio === 'landscape') { - style += 'padding-top: 56.25%;'; - } else if (this.videoRatio === 'portrait') { - style += 'padding-top: 177.78%;'; - } else { - style += 'padding-top: 177.78%;'; - } + style += 'padding-top: ' + (ratio * 100) + '%;'; // #endif // #ifdef MP-HARMONY style += 'width: 100%;'; - style += 'height: ' + this.harmonyVideoHeight + 'px;'; + style += 'height: ' + (this.windowWidth * ratio) + 'px;'; // #endif return style; - }, - - // 鸿蒙平台视频高度 - harmonyVideoHeight() { - if (!this.videoRatio) { - return (this.windowWidth * 9) / 16; - } - if (this.videoRatio === 'landscape') { - return (this.windowWidth * 9) / 16; - } - return (this.windowWidth * 16) / 9; } }, methods: { @@ -192,7 +176,8 @@ export default { const height = video.videoHeight; if (width && height) { - this.videoRatio = width >= height ? 'landscape' : 'portrait'; + this.videoWidth = width; + this.videoHeight = height; } }); @@ -231,7 +216,8 @@ export default { // #endif if (width && height) { - this.videoRatio = width >= height ? 'landscape' : 'portrait'; + this.videoWidth = width; + this.videoHeight = height; } }, @@ -277,6 +263,11 @@ export default { /* H5端样式 */ /* #ifdef H5 */ .h5-video { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; object-fit: cover; } /* #endif */ From cb9234bdac5b13240d896f2b30c0c86ce2579090 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 14:41:29 +0800 Subject: [PATCH 09/46] =?UTF-8?q?refactor(diy-video-list):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=BC=B9=E7=AA=97=E8=A7=86=E9=A2=91=E9=80=82=E9=85=8D?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构视频比例计算逻辑,改为通过实际宽高计算适配 2. 新增多端适配:H5、微信小程序、鸿蒙元服务 3. 添加视频加载错误捕获与日志输出 4. 调整视频容器样式,统一背景与溢出处理 5. 移除旧的比例状态变量,改用宽高状态存储 --- components-diy/diy-video-list.vue | 104 ++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 26 deletions(-) diff --git a/components-diy/diy-video-list.vue b/components-diy/diy-video-list.vue index 53a8f06..2b651f2 100644 --- a/components-diy/diy-video-list.vue +++ b/components-diy/diy-video-list.vue @@ -64,7 +64,9 @@ id="myVideo" :src="video_url" :controls="true" + object-fit="contain" @loadedmetadata="onPopupVideoLoaded" + @error="onPopupVideoError" > @@ -88,11 +90,14 @@ export default { pageWidth: '', indicatorDots: false, swiperCurrent: 0, - video_url: '' + video_url: '', + popupVideoWidth: 0, + popupVideoHeight: 0, + windowWidth: 0 } }, created() { - // 组件创建时的逻辑 + this.initWindowWidth(); }, watch: { componentRefresh(newValue) { @@ -140,24 +145,33 @@ export default { // 弹窗视频容器样式 popupVideoStyle() { let style = 'position:fixed;width:100%;left:0;'; - - if (this.popupVideoRatio) { - // 根据视频比例设置位置 - if (this.popupVideoRatio === 'portrait') { - // 竖屏视频:居中显示,使用9/16比例 - style += 'top:50%;transform:translateY(-50%);'; - style += 'aspect-ratio: 9 / 16;'; - } else { - // 横屏视频:靠近顶部显示,使用16/9比例 - style += 'top:20%;'; - style += 'aspect-ratio: 16 / 9;'; - } - } else { - // 默认:居中显示,使用竖屏比例 - style += 'top:50%;transform:translateY(-50%);'; - style += 'aspect-ratio: 9 / 16;'; + const defaultRatio = 9 / 16; + + let ratio = defaultRatio; + if (this.popupVideoWidth && this.popupVideoHeight) { + ratio = this.popupVideoHeight / this.popupVideoWidth; } - + + const isPortrait = ratio > 1; + + if (isPortrait) { + style += 'top:50%;transform:translateY(-50%);'; + } else { + style += 'top:20%;'; + } + + // #ifdef H5 + style += 'padding-top: ' + (ratio * 100) + '%;'; + // #endif + + // #ifdef MP-WEIXIN + style += 'padding-top: ' + (ratio * 100) + '%;'; + // #endif + + // #ifdef MP-HARMONY + style += 'height: ' + (this.windowWidth * ratio) + 'px;'; + // #endif + return style; } }, @@ -165,8 +179,8 @@ export default { // 显示视频播放弹窗 showVideo(item) { this.video_url = item.videoUrl; - // 重置视频比例为null,等待新的检测结果 - this.popupVideoRatio = null; + this.popupVideoWidth = 0; + this.popupVideoHeight = 0; this.$refs.videoPopup.open(); }, @@ -174,6 +188,16 @@ export default { imgError(index) { // 图片加载失败的处理逻辑 }, + + // 初始化窗口宽度 + initWindowWidth() { + try { + const systemInfo = uni.getSystemInfoSync(); + this.windowWidth = systemInfo.windowWidth || 375; + } catch (e) { + this.windowWidth = 375; + } + }, // 弹窗视频加载完成事件 onPopupVideoLoaded(event) { @@ -198,13 +222,24 @@ export default { } // #endif + // 鸿蒙元服务端:通过事件对象获取视频宽高 + // #ifdef MP-HARMONY + if (event && event.detail) { + width = event.detail.width || 0; + height = event.detail.height || 0; + } + // #endif + if (width && height) { - this.popupVideoRatio = width >= height ? 'landscape' : 'portrait'; - } else { - // 获取失败,默认竖屏 - this.popupVideoRatio = 'portrait'; + this.popupVideoWidth = width; + this.popupVideoHeight = height; } }); + }, + + // 弹窗视频错误事件 + onPopupVideoError(event) { + console.error('Popup video load failed:', event); } } } @@ -576,8 +611,9 @@ export default { .video-container { width: 100%; - height: 300px; position: relative; + overflow: hidden; + background-color: #000; } .adaptive-video { @@ -589,6 +625,22 @@ export default { left: 0; } +/* #ifdef MP-WEIXIN */ +.adaptive-video { + position: absolute; + top: 0; + left: 0; +} +/* #endif */ + +/* #ifdef MP-HARMONY */ +.adaptive-video { + position: static; + display: block; + box-sizing: border-box; +} +/* #endif */ + .video-nav { padding: 16rpx; box-sizing: border-box; From 30cda717493af910430834223d990a9ab58301ea Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Mon, 13 Jul 2026 15:45:15 +0800 Subject: [PATCH 10/46] =?UTF-8?q?refactor(http,lang):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E9=80=BB=E8=BE=91=E5=B9=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增网络相关错误的多语言翻译配置 2. 重写http请求层,添加自动重试机制与错误分类提示 3. 优化鸿蒙平台下的组件渲染兼容性与数据更新逻辑 4. 调整部分页面的模板结构与请求配置 --- common/js/diy.js | 6 +- common/js/http.js | 257 ++++++++++++++------------- components/ns-payment/ns-payment.vue | 2 + lang/en-us/common.js | 10 ++ lang/zh-cn/common.js | 10 ++ pages/index/index.vue | 40 ++--- pages_goods/category.vue | 56 ++++-- pages_order/public/js/orderMethod.js | 10 ++ 8 files changed, 234 insertions(+), 157 deletions(-) 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_tool/member/card_buy.vue b/pages_tool/member/card_buy.vue index f2c8edc..a08d1c0 100644 --- a/pages_tool/member/card_buy.vue +++ b/pages_tool/member/card_buy.vue @@ -215,6 +215,7 @@ + diff --git a/pages_tool/member/index.vue b/pages_tool/member/index.vue index 6700c81..70ea90f 100644 --- a/pages_tool/member/index.vue +++ b/pages_tool/member/index.vue @@ -71,12 +71,14 @@ +