xsh_1997 hai 2 meses
pai
achega
df2b9309e7
Modificáronse 33 ficheiros con 664 adicións e 411 borrados
  1. 38 36
      ruoyi-jiaoyi/App.vue
  2. 2 5
      ruoyi-jiaoyi/components/distributor/home-panel.vue
  3. 3 9
      ruoyi-jiaoyi/components/distributor/order-panel.vue
  4. 3 4
      ruoyi-jiaoyi/components/partner/guest-view.vue
  5. 2 2
      ruoyi-jiaoyi/components/partner/placeholder.vue
  6. 2 5
      ruoyi-jiaoyi/components/supplier/home-panel.vue
  7. 4 10
      ruoyi-jiaoyi/components/supplier/market-panel.vue
  8. 3 9
      ruoyi-jiaoyi/components/supplier/order-panel.vue
  9. 2 1
      ruoyi-jiaoyi/main.js
  10. 3 4
      ruoyi-jiaoyi/pages/distributor/order-detail/index.vue
  11. 3 4
      ruoyi-jiaoyi/pages/distributor/yak-detail/index.vue
  12. 1 7
      ruoyi-jiaoyi/pages/home/index.vue
  13. 1 7
      ruoyi-jiaoyi/pages/market/index.vue
  14. 8 5
      ruoyi-jiaoyi/pages/mine/index.vue
  15. 1 7
      ruoyi-jiaoyi/pages/order/index.vue
  16. 3 4
      ruoyi-jiaoyi/pages/supplier/order-detail/index.vue
  17. 28 0
      ruoyi-jiaoyi/styles/detail-page.scss
  18. 53 0
      ruoyi-jiaoyi/styles/h5-layout.scss
  19. 20 0
      ruoyi-jiaoyi/styles/scroll-fill.scss
  20. 6 0
      ruoyi-jiaoyi/styles/tab-page.scss
  21. 23 10
      ruoyi-jiaoyi/uni.promisify.adaptor.js
  22. 63 0
      ruoyi-jiaoyi/utils/tabBar.js
  23. 9 1
      ruoyi-ui-app/.env.production
  24. 40 39
      ruoyi-ui-app/App.vue
  25. 2 2
      ruoyi-ui-app/main.js
  26. 4 1
      ruoyi-ui-app/mixins/tabPage.js
  27. 4 3
      ruoyi-ui-app/package-a/consult-detail/index.vue
  28. 214 213
      ruoyi-ui-app/package-a/course-detail/index.vue
  29. 4 3
      ruoyi-ui-app/package-a/training-detail/index.vue
  30. 13 1
      ruoyi-ui-app/pages/mine/index.vue
  31. 23 10
      ruoyi-ui-app/uni.promisify.adaptor.js
  32. 61 9
      ruoyi-ui-app/utils/tabBar.js
  33. 18 0
      ruoyi-ui-app/vite.config.js

+ 38 - 36
ruoyi-jiaoyi/App.vue

@@ -1,36 +1,38 @@
1
-<script>

2
-import { syncUserOnLaunch } from '@/permission'

3
-

4
-export default {

5
-  onLaunch() {

6
-    syncUserOnLaunch()

7
-  },

8
-  onShow() {

9
-    console.log('App Show')

10
-  },

11
-  onHide() {

12
-    console.log('App Hide')

13
-  }

14
-}

15
-</script>

16
-

17
-<style lang="scss">

18
-@import 'uview-plus/index.scss';

19
-

20
-/* #ifdef H5 */

21
-page {

22
-  min-height: 100%;

23
-  height: auto;

24
-  box-sizing: border-box;

25
-  background-color: #f0ebe5;

26
-}

27
-/* #endif */

28
-

29
-/* #ifndef H5 */

30
-page {

31
-  height: 100%;

32
-  min-height: 100%;

33
-  background-color: #f0ebe5;

34
-}

35
-/* #endif */

36
-</style>

1
+<script>
2
+import { syncUserOnLaunch } from '@/permission'
3
+
4
+export default {
5
+  onLaunch() {
6
+    syncUserOnLaunch().catch(() => {})
7
+  },
8
+  onShow() {
9
+    console.log('App Show')
10
+  },
11
+  onHide() {
12
+    console.log('App Hide')
13
+  }
14
+}
15
+</script>
16
+
17
+<style lang="scss">
18
+@import 'uview-plus/index.scss';
19
+@import '@/styles/h5-layout.scss';
20
+
21
+/* Tab 页见 h5-layout;栈式详情页用页面内 scroll-view + detail-page.scss */
22
+/* #ifdef H5 */
23
+page {
24
+  min-height: 100%;
25
+  height: auto;
26
+  box-sizing: border-box;
27
+  background-color: #f0ebe5;
28
+}
29
+/* #endif */
30
+
31
+/* #ifndef H5 */
32
+page {
33
+  height: 100%;
34
+  min-height: 100%;
35
+  background-color: #f0ebe5;
36
+}
37
+/* #endif */
38
+</style>

+ 2 - 5
ruoyi-jiaoyi/components/distributor/home-panel.vue

@@ -203,13 +203,10 @@ export default {
203
 
203
 
204
 <style lang="scss" scoped>
204
 <style lang="scss" scoped>
205
 @import '@/styles/morandi.scss';
205
 @import '@/styles/morandi.scss';
206
+@import '@/styles/scroll-fill.scss';
206
 
207
 
207
 .sp-scroll {
208
 .sp-scroll {
208
-  flex: 1;
209
-  min-height: 0;
210
-  height: 0;
211
-  min-width: 0;
212
-  box-sizing: border-box;
209
+  @include flex-scroll-y;
213
   background: $morandi-bg-page;
210
   background: $morandi-bg-page;
214
 }
211
 }
215
 
212
 

+ 3 - 9
ruoyi-jiaoyi/components/distributor/order-panel.vue

@@ -169,13 +169,10 @@ export default {
169
 
169
 
170
 <style lang="scss" scoped>
170
 <style lang="scss" scoped>
171
 @import '@/styles/morandi.scss';
171
 @import '@/styles/morandi.scss';
172
+@import '@/styles/scroll-fill.scss';
172
 
173
 
173
 .sp-order-page {
174
 .sp-order-page {
174
-  display: flex;
175
-  flex-direction: column;
176
-  flex: 1;
177
-  min-height: 0;
178
-  min-width: 0;
175
+  @include tab-panel-root;
179
   background: $morandi-bg-page;
176
   background: $morandi-bg-page;
180
 }
177
 }
181
 
178
 
@@ -212,10 +209,7 @@ export default {
212
 }
209
 }
213
 
210
 
214
 .sp-order-scroll {
211
 .sp-order-scroll {
215
-  flex: 1;
216
-  min-height: 0;
217
-  height: 0;
218
-  min-width: 0;
212
+  @include flex-scroll-y;
219
 }
213
 }
220
 
214
 
221
 .sp-order-list {
215
 .sp-order-list {

+ 3 - 4
ruoyi-jiaoyi/components/partner/guest-view.vue

@@ -21,12 +21,11 @@ export default {
21
 
21
 
22
 <style lang="scss" scoped>
22
 <style lang="scss" scoped>
23
 @import '@/styles/morandi.scss';
23
 @import '@/styles/morandi.scss';
24
+@import '@/styles/scroll-fill.scss';
24
 
25
 
25
 .guest-view {
26
 .guest-view {
26
-  flex: 1;
27
-  min-height: 0;
28
-  display: flex;
29
-  flex-direction: column;
27
+  @include tab-panel-root;
28
+  width: 100%;
30
   align-items: center;
29
   align-items: center;
31
   justify-content: center;
30
   justify-content: center;
32
   padding: 48rpx 40rpx;
31
   padding: 48rpx 40rpx;

+ 2 - 2
ruoyi-jiaoyi/components/partner/placeholder.vue

@@ -22,10 +22,10 @@ export default {
22
 
22
 
23
 <style lang="scss" scoped>
23
 <style lang="scss" scoped>
24
 @import '@/styles/morandi.scss';
24
 @import '@/styles/morandi.scss';
25
+@import '@/styles/scroll-fill.scss';
25
 
26
 
26
 .partner-placeholder {
27
 .partner-placeholder {
27
-  flex: 1;
28
-  min-height: 0;
28
+  @include tab-panel-root;
29
   display: flex;
29
   display: flex;
30
   flex-direction: column;
30
   flex-direction: column;
31
   align-items: center;
31
   align-items: center;

+ 2 - 5
ruoyi-jiaoyi/components/supplier/home-panel.vue

@@ -224,13 +224,10 @@ export default {
224
 
224
 
225
 <style lang="scss" scoped>
225
 <style lang="scss" scoped>
226
 @import '@/styles/morandi.scss';
226
 @import '@/styles/morandi.scss';
227
+@import '@/styles/scroll-fill.scss';
227
 
228
 
228
 .sp-scroll {
229
 .sp-scroll {
229
-  flex: 1;
230
-  min-height: 0;
231
-  height: 0;
232
-  min-width: 0;
233
-  box-sizing: border-box;
230
+  @include flex-scroll-y;
234
   background: $morandi-bg-page;
231
   background: $morandi-bg-page;
235
 }
232
 }
236
 
233
 

+ 4 - 10
ruoyi-jiaoyi/components/supplier/market-panel.vue

@@ -582,22 +582,16 @@ export default {
582
 @import '@/styles/morandi.scss';
582
 @import '@/styles/morandi.scss';
583
 @import '@/styles/tab-page.scss';
583
 @import '@/styles/tab-page.scss';
584
 @import '@/styles/app-lang.scss';
584
 @import '@/styles/app-lang.scss';
585
+@import '@/styles/scroll-fill.scss';
585
 
586
 
586
 .ym-page {
587
 .ym-page {
587
-  display: flex;
588
-  flex-direction: column;
589
-  min-width: 0;
590
-  min-height: 100%;
591
-  box-sizing: border-box;
588
+  @include tab-panel-root;
589
+  overflow: hidden;
592
   background: $morandi-bg-page;
590
   background: $morandi-bg-page;
593
 }
591
 }
594
 
592
 
595
 .ym-scroll {
593
 .ym-scroll {
596
-  flex: 1;
597
-  min-height: 0;
598
-  min-width: 0;
599
-  height: 0;
600
-  box-sizing: border-box;
594
+  @include flex-scroll-y;
601
 }
595
 }
602
 
596
 
603
 .ym-hero {
597
 .ym-hero {

+ 3 - 9
ruoyi-jiaoyi/components/supplier/order-panel.vue

@@ -177,13 +177,10 @@ export default {
177
 
177
 
178
 <style lang="scss" scoped>
178
 <style lang="scss" scoped>
179
 @import '@/styles/morandi.scss';
179
 @import '@/styles/morandi.scss';
180
+@import '@/styles/scroll-fill.scss';
180
 
181
 
181
 .sp-order-page {
182
 .sp-order-page {
182
-  display: flex;
183
-  flex-direction: column;
184
-  flex: 1;
185
-  min-height: 0;
186
-  min-width: 0;
183
+  @include tab-panel-root;
187
   background: $morandi-bg-page;
184
   background: $morandi-bg-page;
188
 }
185
 }
189
 
186
 
@@ -220,10 +217,7 @@ export default {
220
 }
217
 }
221
 
218
 
222
 .sp-order-scroll {
219
 .sp-order-scroll {
223
-  flex: 1;
224
-  min-height: 0;
225
-  height: 0;
226
-  min-width: 0;
220
+  @include flex-scroll-y;
227
 }
221
 }
228
 
222
 
229
 .sp-order-list {
223
 .sp-order-list {

+ 2 - 1
ruoyi-jiaoyi/main.js

@@ -1,8 +1,9 @@
1
+import './uni.promisify.adaptor'
2
+import '@/utils/tabBar'
1
 import App from './App'
3
 import App from './App'
2
 
4
 
3
 // #ifndef VUE3
5
 // #ifndef VUE3
4
 import Vue from 'vue'
6
 import Vue from 'vue'
5
-import './uni.promisify.adaptor'
6
 Vue.config.productionTip = false
7
 Vue.config.productionTip = false
7
 App.mpType = 'app'
8
 App.mpType = 'app'
8
 const app = new Vue({
9
 const app = new Vue({

+ 3 - 4
ruoyi-jiaoyi/pages/distributor/order-detail/index.vue

@@ -155,15 +155,14 @@ export default {
155
 
155
 
156
 <style lang="scss" scoped>
156
 <style lang="scss" scoped>
157
 @import '@/styles/morandi.scss';
157
 @import '@/styles/morandi.scss';
158
+@import '@/styles/detail-page.scss';
158
 
159
 
159
 .sp-detail-page {
160
 .sp-detail-page {
160
-  min-height: 100%;
161
-  background: $morandi-bg-page;
161
+  @include detail-page-root;
162
 }
162
 }
163
 
163
 
164
 .sp-detail-scroll {
164
 .sp-detail-scroll {
165
-  min-height: 100%;
166
-  box-sizing: border-box;
165
+  @include detail-page-scroll;
167
 }
166
 }
168
 
167
 
169
 .sp-block {
168
 .sp-block {

+ 3 - 4
ruoyi-jiaoyi/pages/distributor/yak-detail/index.vue

@@ -119,15 +119,14 @@ export default {
119
 
119
 
120
 <style lang="scss" scoped>
120
 <style lang="scss" scoped>
121
 @import '@/styles/morandi.scss';
121
 @import '@/styles/morandi.scss';
122
+@import '@/styles/detail-page.scss';
122
 
123
 
123
 .sp-detail-page {
124
 .sp-detail-page {
124
-  min-height: 100%;
125
-  background: $morandi-bg-page;
125
+  @include detail-page-root;
126
 }
126
 }
127
 
127
 
128
 .sp-detail-scroll {
128
 .sp-detail-scroll {
129
-  min-height: 100%;
130
-  box-sizing: border-box;
129
+  @include detail-page-scroll;
131
 }
130
 }
132
 
131
 
133
 .sp-block {
132
 .sp-block {

+ 1 - 7
ruoyi-jiaoyi/pages/home/index.vue

@@ -66,11 +66,5 @@ export default {
66
 </script>
66
 </script>
67
 
67
 
68
 <style lang="scss" scoped>
68
 <style lang="scss" scoped>
69
-.tab-page {
70
-  display: flex;
71
-  flex-direction: column;
72
-  min-width: 0;
73
-  min-height: 100%;
74
-  box-sizing: border-box;
75
-}
69
+@import '@/styles/tab-page.scss';
76
 </style>
70
 </style>

+ 1 - 7
ruoyi-jiaoyi/pages/market/index.vue

@@ -44,11 +44,5 @@ export default {
44
 </script>
44
 </script>
45
 
45
 
46
 <style lang="scss" scoped>
46
 <style lang="scss" scoped>
47
-.tab-page {
48
-  display: flex;
49
-  flex-direction: column;
50
-  min-width: 0;
51
-  min-height: 100%;
52
-  box-sizing: border-box;
53
-}
47
+@import '@/styles/tab-page.scss';
54
 </style>
48
 </style>

+ 8 - 5
ruoyi-jiaoyi/pages/mine/index.vue

@@ -130,6 +130,7 @@ export default {
130
 
130
 
131
 <style lang="scss" scoped>
131
 <style lang="scss" scoped>
132
 @import '@/styles/morandi.scss';
132
 @import '@/styles/morandi.scss';
133
+@import '@/styles/scroll-fill.scss';
133
 
134
 
134
 .mine-page {
135
 .mine-page {
135
   display: flex;
136
   display: flex;
@@ -138,15 +139,17 @@ export default {
138
   min-height: 100%;
139
   min-height: 100%;
139
   background: $morandi-bg-page;
140
   background: $morandi-bg-page;
140
   box-sizing: border-box;
141
   box-sizing: border-box;
142
+  /* #ifdef H5 */
143
+  flex: 1;
144
+  height: 100%;
145
+  min-height: 0;
146
+  overflow: hidden;
147
+  /* #endif */
141
 }
148
 }
142
 
149
 
143
 .mine-scroll {
150
 .mine-scroll {
144
-  flex: 1;
145
-  min-height: 0;
146
-  height: 0;
147
-  min-width: 0;
151
+  @include flex-scroll-y;
148
   padding: 24rpx 24rpx 40rpx;
152
   padding: 24rpx 24rpx 40rpx;
149
-  box-sizing: border-box;
150
 }
153
 }
151
 
154
 
152
 .mine-profile {
155
 .mine-profile {

+ 1 - 7
ruoyi-jiaoyi/pages/order/index.vue

@@ -60,11 +60,5 @@ export default {
60
 </script>
60
 </script>
61
 
61
 
62
 <style lang="scss" scoped>
62
 <style lang="scss" scoped>
63
-.tab-page {
64
-  display: flex;
65
-  flex-direction: column;
66
-  min-width: 0;
67
-  min-height: 100%;
68
-  box-sizing: border-box;
69
-}
63
+@import '@/styles/tab-page.scss';
70
 </style>
64
 </style>

+ 3 - 4
ruoyi-jiaoyi/pages/supplier/order-detail/index.vue

@@ -183,15 +183,14 @@ export default {
183
 
183
 
184
 <style lang="scss" scoped>
184
 <style lang="scss" scoped>
185
 @import '@/styles/morandi.scss';
185
 @import '@/styles/morandi.scss';
186
+@import '@/styles/detail-page.scss';
186
 
187
 
187
 .sp-detail-page {
188
 .sp-detail-page {
188
-  min-height: 100%;
189
-  background: $morandi-bg-page;
189
+  @include detail-page-root;
190
 }
190
 }
191
 
191
 
192
 .sp-detail-scroll {
192
 .sp-detail-scroll {
193
-  min-height: 100%;
194
-  box-sizing: border-box;
193
+  @include detail-page-scroll;
195
 }
194
 }
196
 
195
 
197
 .sp-block {
196
 .sp-block {

+ 28 - 0
ruoyi-jiaoyi/styles/detail-page.scss

@@ -0,0 +1,28 @@
1
+@import './morandi.scss';
2
+
3
+/** 栈式详情页(订单详情、牦牛详情等):scroll-view 须有明确高度才能滚动 */
4
+@mixin detail-page-root {
5
+  min-height: 100%;
6
+  box-sizing: border-box;
7
+  background: $morandi-bg-page;
8
+  /* #ifdef H5 */
9
+  height: 100vh;
10
+  height: 100dvh;
11
+  display: flex;
12
+  flex-direction: column;
13
+  overflow: hidden;
14
+  /* #endif */
15
+}
16
+
17
+@mixin detail-page-scroll {
18
+  width: 100%;
19
+  box-sizing: border-box;
20
+  /* #ifdef H5 */
21
+  flex: 1;
22
+  min-height: 0;
23
+  height: 100%;
24
+  /* #endif */
25
+  /* #ifndef H5 */
26
+  min-height: 100%;
27
+  /* #endif */
28
+}

+ 53 - 0
ruoyi-jiaoyi/styles/h5-layout.scss

@@ -0,0 +1,53 @@
1
+/**
2
+ * 仅作用于 Tab 页(.tab-page),勿改全局 page,避免详情等栈式页面无法滚动
3
+ */
4
+/* #ifdef H5 */
5
+.tab-page {
6
+  flex: 1 1 auto;
7
+  width: 100%;
8
+  height: 100vh;
9
+  height: 100dvh;
10
+  min-height: 100vh;
11
+  min-height: 100dvh;
12
+  min-height: 0;
13
+  overflow: hidden;
14
+  display: flex;
15
+  flex-direction: column;
16
+  box-sizing: border-box;
17
+}
18
+
19
+.tab-page > view,
20
+.tab-page > scroll-view {
21
+  flex: 1 1 auto;
22
+  min-height: 0;
23
+  height: 100%;
24
+  width: 100%;
25
+  display: flex;
26
+  flex-direction: column;
27
+  box-sizing: border-box;
28
+}
29
+
30
+.sp-scroll,
31
+.sp-order-page,
32
+.sp-order-scroll,
33
+.ym-page,
34
+.ym-scroll,
35
+.mine-page,
36
+.mine-scroll,
37
+.guest-view {
38
+  flex: 1 1 auto;
39
+  min-height: 0;
40
+  height: 100%;
41
+  width: 100%;
42
+  box-sizing: border-box;
43
+}
44
+
45
+scroll-view.sp-scroll,
46
+scroll-view.sp-order-scroll,
47
+scroll-view.ym-scroll,
48
+scroll-view.mine-scroll {
49
+  height: 100% !important;
50
+  min-height: 1px !important;
51
+  flex: 1 1 auto !important;
52
+}
53
+/* #endif */

+ 20 - 0
ruoyi-jiaoyi/styles/scroll-fill.scss

@@ -0,0 +1,20 @@
1
+/** Tab 内可滚动区域:iOS 上 flex + height:0 会塌成空白,统一用 100% + min-height:1px */
2
+@mixin flex-scroll-y {
3
+  flex: 1 1 auto;
4
+  min-height: 1px;
5
+  min-width: 0;
6
+  width: 100%;
7
+  height: 100%;
8
+  box-sizing: border-box;
9
+}
10
+
11
+@mixin tab-panel-root {
12
+  display: flex;
13
+  flex-direction: column;
14
+  flex: 1 1 auto;
15
+  min-width: 0;
16
+  min-height: 0;
17
+  width: 100%;
18
+  height: 100%;
19
+  box-sizing: border-box;
20
+}

+ 6 - 0
ruoyi-jiaoyi/styles/tab-page.scss

@@ -128,6 +128,12 @@
128
   min-width: 0;
128
   min-width: 0;
129
   min-height: 100%;
129
   min-height: 100%;
130
   box-sizing: border-box;
130
   box-sizing: border-box;
131
+  /* #ifdef H5 */
132
+  flex: 1;
133
+  height: 100%;
134
+  min-height: 0;
135
+  overflow: hidden;
136
+  /* #endif */
131
 }
137
 }
132
 
138
 
133
 .pkg-entry {
139
 .pkg-entry {

+ 23 - 10
ruoyi-jiaoyi/uni.promisify.adaptor.js

@@ -1,13 +1,26 @@
1
+function isTabBarFail(err) {
2
+  const msg = err && err.errMsg ? String(err.errMsg) : ''
3
+  return msg.includes('TabBar') && msg.includes('fail')
4
+}
5
+
1
 uni.addInterceptor({
6
 uni.addInterceptor({
2
-  returnValue (res) {
3
-    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
4
-      return res;
7
+  returnValue(res) {
8
+    if (!(!!res && (typeof res === 'object' || typeof res === 'function') && typeof res.then === 'function')) {
9
+      return res
5
     }
10
     }
6
     return new Promise((resolve, reject) => {
11
     return new Promise((resolve, reject) => {
7
-      res.then((res) => {
8
-        if (!res) return resolve(res) 
9
-        return res[0] ? reject(res[0]) : resolve(res[1])
10
-      });
11
-    });
12
-  },
13
-});
12
+      res.then((tuple) => {
13
+        if (!tuple) {
14
+          return resolve(tuple)
15
+        }
16
+        if (tuple[0]) {
17
+          if (isTabBarFail(tuple[0])) {
18
+            return resolve(tuple[0])
19
+          }
20
+          return reject(tuple[0])
21
+        }
22
+        return resolve(tuple[1])
23
+      })
24
+    })
25
+  }
26
+})

+ 63 - 0
ruoyi-jiaoyi/utils/tabBar.js

@@ -0,0 +1,63 @@
1
+/** 与 pages.json tabBar.list[].pagePath 一致(无开头 /) */
2
+const TAB_PAGE_PATHS = new Set([
3
+  'pages/home/index',
4
+  'pages/order/index',
5
+  'pages/market/index',
6
+  'pages/mine/index'
7
+])
8
+
9
+const TABBAR_API_NAMES = [
10
+  'setTabBarStyle',
11
+  'setTabBarItem',
12
+  'setTabBarBadge',
13
+  'removeTabBarBadge',
14
+  'showTabBarRedDot',
15
+  'hideTabBarRedDot',
16
+  'showTabBar',
17
+  'hideTabBar',
18
+  'setTabBarMidButton'
19
+]
20
+
21
+function normalizePageRoute(route) {
22
+  if (!route) {
23
+    return ''
24
+  }
25
+  let r = String(route).replace(/^\//, '')
26
+  const prefixes = ['bqH5/', 'bqjy/']
27
+  for (const prefix of prefixes) {
28
+    if (r.startsWith(prefix)) {
29
+      r = r.slice(prefix.length)
30
+    }
31
+  }
32
+  return r
33
+}
34
+
35
+export function isTabBarPage() {
36
+  const stack = getCurrentPages()
37
+  if (!stack.length) {
38
+    return false
39
+  }
40
+  const cur = stack[stack.length - 1]
41
+  const route = normalizePageRoute(cur.route || '')
42
+  return TAB_PAGE_PATHS.has(route)
43
+}
44
+
45
+function installTabBarApiGuard() {
46
+  if (typeof uni === 'undefined' || !uni.addInterceptor) {
47
+    return
48
+  }
49
+  TABBAR_API_NAMES.forEach((name) => {
50
+    if (typeof uni[name] !== 'function') {
51
+      return
52
+    }
53
+    uni.addInterceptor(name, {
54
+      invoke() {
55
+        if (!isTabBarPage()) {
56
+          return false
57
+        }
58
+      }
59
+    })
60
+  })
61
+}
62
+
63
+installTabBarApiGuard()

+ 9 - 1
ruoyi-ui-app/.env.production

@@ -1,3 +1,11 @@
1
-# 生产环境(对齐 ruoyi-ui/.env.production)
1
+# 页面标题(对齐 ruoyi-ui/.env.production)
2
 VITE_APP_TITLE=掌上巴青
2
 VITE_APP_TITLE=掌上巴青
3
+
4
+# 生产环境
5
+ENV=production
6
+
7
+# 若依后端 API(与 ruoyi-ui 一致,由 Nginx 反代到真实后端)
3
 VITE_APP_BASE_API=/prod-api
8
 VITE_APP_BASE_API=/prod-api
9
+
10
+# H5 部署子路径(须与 manifest.json h5.router.base 一致,如 /bqH5/)
11
+VITE_APP_PUBLIC_PATH=bqH5

+ 40 - 39
ruoyi-ui-app/App.vue

@@ -1,39 +1,40 @@
1
-<script>
2
-import { syncUserOnLaunch } from '@/permission'
3
-
4
-export default {
5
-  onLaunch() {
6
-    syncUserOnLaunch()
7
-  },
8
-  onShow() {
9
-    console.log('App Show')
10
-  },
11
-  onHide() {
12
-    console.log('App Hide')
13
-  }
14
-}
15
-</script>
16
-
17
-<style lang="scss">
18
-/* 须写在首行:与 uni.scss 中的 theme 配合,否则 index 内 $u-* 未定义 */
19
-@import 'uview-plus/index.scss';
20
-@import '@/styles/app-lang.scss';
21
-
22
-/* 全屏高度链:H5 若写死 page height:100%,易与原生 tabBar 抢高度导致底栏「看不见」 */
23
-/* #ifdef H5 */
24
-page {
25
-  min-height: 100%;
26
-  height: auto;
27
-  box-sizing: border-box;
28
-  background-color: #f0ebe5;
29
-}
30
-/* #endif */
31
-
32
-/* #ifndef H5 */
33
-page {
34
-  height: 100%;
35
-  min-height: 100%;
36
-  background-color: #f0ebe5;
37
-}
38
-/* #endif */
39
-</style>
1
+<script>

2
+import { syncUserOnLaunch } from '@/permission'

3
+

4
+export default {

5
+  onLaunch() {

6
+    syncUserOnLaunch().catch(() => {})

7
+  },

8
+  onShow() {

9
+    console.log('App Show')

10
+  },

11
+  onHide() {

12
+    console.log('App Hide')

13
+  }

14
+}

15
+</script>

16
+

17
+<style lang="scss">

18
+/* 须写在首行:与 uni.scss 中的 theme 配合,否则 index 内 $u-* 未定义 */

19
+@import 'uview-plus/index.scss';

20
+@import '@/styles/app-lang.scss';

21
+

22
+/* 全屏高度链:H5 若写死 page height:100%,易与原生 tabBar 抢高度导致底栏「看不见」 */

23
+/* #ifdef H5 */

24
+page {

25
+  min-height: 100%;

26
+  height: auto;

27
+  box-sizing: border-box;

28
+  background-color: #f0ebe5;

29
+}

30
+/* #endif */

31
+

32
+/* #ifndef H5 */

33
+page {

34
+  height: 100%;

35
+  min-height: 100%;

36
+  background-color: #f0ebe5;

37
+}

38
+/* #endif */

39
+</style>

40
+

+ 2 - 2
ruoyi-ui-app/main.js

@@ -1,9 +1,9 @@
1
+import './uni.promisify.adaptor'
2
+import '@/utils/tabBar'
1
 import App from './App'
3
 import App from './App'
2
 
4
 
3
-
4
 // #ifndef VUE3
5
 // #ifndef VUE3
5
 import Vue from 'vue'
6
 import Vue from 'vue'
6
-import './uni.promisify.adaptor'
7
 Vue.config.productionTip = false
7
 Vue.config.productionTip = false
8
 App.mpType = 'app'
8
 App.mpType = 'app'
9
 const app = new Vue({
9
 const app = new Vue({

+ 4 - 1
ruoyi-ui-app/mixins/tabPage.js

@@ -22,9 +22,12 @@ export default {
22
     syncTabBarText((k) => this.$t(k))
22
     syncTabBarText((k) => this.$t(k))
23
     const key = this.navTitleKey
23
     const key = this.navTitleKey
24
     if (key) {
24
     if (key) {
25
-      uni.setNavigationBarTitle({
25
+      const p = uni.setNavigationBarTitle({
26
         title: this.$t(key)
26
         title: this.$t(key)
27
       })
27
       })
28
+      if (p && typeof p.catch === 'function') {
29
+        p.catch(() => {})
30
+      }
28
     }
31
     }
29
   }
32
   }
30
 }
33
 }

+ 4 - 3
ruoyi-ui-app/package-a/consult-detail/index.vue

@@ -166,7 +166,6 @@
166
 import UButton from 'uview-plus/components/u-button/u-button.vue'
166
 import UButton from 'uview-plus/components/u-button/u-button.vue'
167
 import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
167
 import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
168
 import tabPage from '@/mixins/tabPage'
168
 import tabPage from '@/mixins/tabPage'
169
-import { syncTabBarText } from '@/utils/tabBar'
170
 import { ensureApiToken } from '@/utils/apiAuth'
169
 import { ensureApiToken } from '@/utils/apiAuth'
171
 import { uploadFile } from '@/utils/upload'
170
 import { uploadFile } from '@/utils/upload'
172
 import {
171
 import {
@@ -278,10 +277,12 @@ export default {
278
     setTimeout(() => uni.navigateBack(), 1500)
277
     setTimeout(() => uni.navigateBack(), 1500)
279
   },
278
   },
280
   onShow() {
279
   onShow() {
281
-    syncTabBarText((k) => this.$t(k))
282
-    uni.setNavigationBarTitle({
280
+    const p = uni.setNavigationBarTitle({
283
       title: this.peerName || this.$t(this.navTitleKey)
281
       title: this.peerName || this.$t(this.navTitleKey)
284
     })
282
     })
283
+    if (p && typeof p.catch === 'function') {
284
+      p.catch(() => {})
285
+    }
285
     if (this.sessionReady) {
286
     if (this.sessionReady) {
286
       this.pollNewMessages()
287
       this.pollNewMessages()
287
       this.startPoll()
288
       this.startPoll()

+ 214 - 213
ruoyi-ui-app/package-a/course-detail/index.vue

@@ -1,213 +1,214 @@
1
-<template>

2
-  <!-- 课程详情(英文路径):上视频、中标题、下日期+简介 -->

3
-  <view :class="pageRootClass" class="tab-page course-detail-page">

4
-    <scroll-view scroll-y class="course-detail-scroll" enable-back-to-top>

5
-      <view class="course-detail-inner">

6
-        <view class="course-detail-top">

7
-          <video

8
-            v-if="videoSrc"

9
-            class="course-detail-video"

10
-            :src="videoSrc"

11
-            :poster="posterSrc"

12
-            controls

13
-            object-fit="contain"

14
-            :enable-progress-gesture="true"

15
-            :show-center-play-btn="true"

16
-          />

17
-          <view v-else class="course-detail-video course-detail-video--empty">

18
-            <text class="text-body course-detail-video__ph">{{ $t('courseDetailPage.noVideo') }}</text>

19
-          </view>

20
-        </view>

21
-        <text class="course-detail-title">{{ displayTitle }}</text>

22
-        <view class="course-detail-bottom">

23
-          <text class="text-body course-detail-date">{{ displayDate }}</text>

24
-          <text class="text-body course-detail-intro">{{ introText }}</text>

25
-        </view>

26
-      </view>

27
-    </scroll-view>

28
-  </view>

29
-</template>

30
-

31
-<script>

32
-import tabPage from '@/mixins/tabPage'

33
-import { syncTabBarText } from '@/utils/tabBar'

34
-import { joinApiUrl } from '@/config'

35
-

36
-const POSTER = '/static/ai/hero.png'

37
-

38
-export default {

39
-  mixins: [tabPage],

40
-  data() {

41
-    return {

42
-      navTitleKey: 'courseDetailPage.navTitle',

43
-      courseId: '',

44
-      displayTitleRaw: '',

45
-      typeCode: '',

46
-      displayDate: '',

47
-      introduction: '',

48
-      videoSrc: '',

49
-      posterSrc: POSTER

50
-    }

51
-  },

52
-  computed: {

53
-    displayTitle() {

54
-      return this.displayTitleRaw || this.$t('courseDetailPage.titleFallback')

55
-    },

56
-    introText() {

57
-      return this.introduction || this.$t('courseDetailPage.noIntro')

58
-    }

59
-  },

60
-  onLoad(query) {

61
-    const q = query || {}

62
-    this.courseId = this.decodeQuery(q, 'id')

63
-    this.displayTitleRaw = this.decodeQuery(q, 'title')

64
-    this.typeCode = this.decodeQuery(q, 'type')

65
-    this.introduction = this.decodeQuery(q, 'introduction')

66
-    this.displayDate = this.decodeQuery(q, 'date') || this.$t('courseDetailPage.noDate')

67
-

68
-    const src = this.decodeQuery(q, 'src')

69
-    this.videoSrc = src ? joinApiUrl(src) : ''

70
-

71
-    const cover = this.decodeQuery(q, 'cover')

72
-    this.posterSrc = cover ? joinApiUrl(cover) : POSTER

73
-  },

74
-  onShow() {

75
-    syncTabBarText((k) => this.$t(k))

76
-    uni.setNavigationBarTitle({

77
-      title: this.displayTitleRaw || this.$t(this.navTitleKey)

78
-    })

79
-  },

80
-  methods: {

81
-    decodeQuery(q, key) {

82
-      const raw = q && q[key]

83
-      if (raw == null || raw === '') {

84
-        return ''

85
-      }

86
-      try {

87
-        return decodeURIComponent(String(raw))

88
-      } catch (e) {

89
-        return String(raw)

90
-      }

91
-    }

92
-  }

93
-}

94
-</script>

95
-

96
-<style lang="scss" scoped>

97
-@import '@/styles/morandi.scss';

98
-@import '@/styles/tab-page.scss';

99
-

100
-.course-detail-page {

101
-  display: flex;

102
-  flex-direction: column;

103
-  min-width: 0;

104
-  min-height: 100%;

105
-  box-sizing: border-box;

106
-  background: $morandi-bg-page;

107
-}

108
-

109
-.course-detail-scroll {

110
-  flex: 1;

111
-  min-height: 0;

112
-  min-width: 0;

113
-}

114
-

115
-.course-detail-inner {

116
-  display: flex;

117
-  flex-direction: column;

118
-  align-items: stretch;

119
-  gap: 28rpx;

120
-  min-width: 0;

121
-  padding: 24rpx 24rpx 48rpx;

122
-  box-sizing: border-box;

123
-}

124
-

125
-.course-detail-top {

126
-  width: 100%;

127
-  min-width: 0;

128
-  border-radius: 16rpx;

129
-  overflow: hidden;

130
-  background: #000;

131
-  border: 1rpx solid $morandi-border;

132
-}

133
-

134
-.course-detail-video {

135
-  display: block;

136
-  width: 100%;

137
-  height: 400rpx;

138
-  background: #1a1a1a;

139
-}

140
-

141
-.course-detail-video--empty {

142
-  display: flex;

143
-  align-items: center;

144
-  justify-content: center;

145
-}

146
-

147
-.course-detail-video__ph {

148
-  color: $morandi-text-muted;

149
-  text-align: center;

150
-  padding: 24rpx;

151
-}

152
-

153
-.course-detail-title {

154
-  display: block;

155
-  width: 100%;

156
-  text-align: center;

157
-  font-size: 36rpx;

158
-  font-weight: 700;

159
-  line-height: 1.45;

160
-  color: #111827;

161
-  word-break: break-word;

162
-  overflow-wrap: anywhere;

163
-}

164
-

165
-.course-detail-bottom {

166
-  display: flex;

167
-  flex-direction: column;

168
-  gap: 16rpx;

169
-  min-width: 0;

170
-  padding: 0 8rpx;

171
-  box-sizing: border-box;

172
-}

173
-

174
-.course-detail-date {

175
-  font-size: 24rpx;

176
-  line-height: 1.5;

177
-  color: $morandi-text-muted;

178
-  text-align: center;

179
-}

180
-

181
-.course-detail-intro {

182
-  font-size: 28rpx;

183
-  line-height: 1.55;

184
-  color: $morandi-text-secondary;

185
-  text-align: left;

186
-  word-break: break-word;

187
-  overflow-wrap: anywhere;

188
-}

189
-

190
-.course-detail-page.lang-bo {

191
-  .course-detail-title {

192
-    font-size: 32rpx;

193
-    line-height: 1.75;

194
-    letter-spacing: 2rpx;

195
-    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;

196
-  }

197
-

198
-  .course-detail-date {

199
-    font-size: 22rpx;

200
-    line-height: 1.75;

201
-    letter-spacing: 2rpx;

202
-    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;

203
-  }

204
-

205
-  .course-detail-intro {

206
-    font-size: 26rpx;

207
-    line-height: 1.75;

208
-    letter-spacing: 2rpx;

209
-    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;

210
-  }

211
-}

212
-</style>

213
-
1
+<template>
2
+  <!-- 课程详情(英文路径):上视频、中标题、下日期+简介 -->
3
+  <view :class="pageRootClass" class="tab-page course-detail-page">
4
+    <scroll-view scroll-y class="course-detail-scroll" enable-back-to-top>
5
+      <view class="course-detail-inner">
6
+        <view class="course-detail-top">
7
+          <video
8
+            v-if="videoSrc"
9
+            class="course-detail-video"
10
+            :src="videoSrc"
11
+            :poster="posterSrc"
12
+            controls
13
+            object-fit="contain"
14
+            :enable-progress-gesture="true"
15
+            :show-center-play-btn="true"
16
+          />
17
+          <view v-else class="course-detail-video course-detail-video--empty">
18
+            <text class="text-body course-detail-video__ph">{{ $t('courseDetailPage.noVideo') }}</text>
19
+          </view>
20
+        </view>
21
+        <text class="course-detail-title">{{ displayTitle }}</text>
22
+        <view class="course-detail-bottom">
23
+          <text class="text-body course-detail-date">{{ displayDate }}</text>
24
+          <text class="text-body course-detail-intro">{{ introText }}</text>
25
+        </view>
26
+      </view>
27
+    </scroll-view>
28
+  </view>
29
+</template>
30
+
31
+<script>
32
+import tabPage from '@/mixins/tabPage'
33
+import { joinApiUrl } from '@/config'
34
+
35
+const POSTER = '/static/ai/hero.png'
36
+
37
+export default {
38
+  mixins: [tabPage],
39
+  data() {
40
+    return {
41
+      navTitleKey: 'courseDetailPage.navTitle',
42
+      courseId: '',
43
+      displayTitleRaw: '',
44
+      typeCode: '',
45
+      displayDate: '',
46
+      introduction: '',
47
+      videoSrc: '',
48
+      posterSrc: POSTER
49
+    }
50
+  },
51
+  computed: {
52
+    displayTitle() {
53
+      return this.displayTitleRaw || this.$t('courseDetailPage.titleFallback')
54
+    },
55
+    introText() {
56
+      return this.introduction || this.$t('courseDetailPage.noIntro')
57
+    }
58
+  },
59
+  onLoad(query) {
60
+    const q = query || {}
61
+    this.courseId = this.decodeQuery(q, 'id')
62
+    this.displayTitleRaw = this.decodeQuery(q, 'title')
63
+    this.typeCode = this.decodeQuery(q, 'type')
64
+    this.introduction = this.decodeQuery(q, 'introduction')
65
+    this.displayDate = this.decodeQuery(q, 'date') || this.$t('courseDetailPage.noDate')
66
+
67
+    const src = this.decodeQuery(q, 'src')
68
+    this.videoSrc = src ? joinApiUrl(src) : ''
69
+
70
+    const cover = this.decodeQuery(q, 'cover')
71
+    this.posterSrc = cover ? joinApiUrl(cover) : POSTER
72
+  },
73
+  onShow() {
74
+    const p = uni.setNavigationBarTitle({
75
+      title: this.displayTitleRaw || this.$t(this.navTitleKey)
76
+    })
77
+    if (p && typeof p.catch === 'function') {
78
+      p.catch(() => {})
79
+    }
80
+  },
81
+  methods: {
82
+    decodeQuery(q, key) {
83
+      const raw = q && q[key]
84
+      if (raw == null || raw === '') {
85
+        return ''
86
+      }
87
+      try {
88
+        return decodeURIComponent(String(raw))
89
+      } catch (e) {
90
+        return String(raw)
91
+      }
92
+    }
93
+  }
94
+}
95
+</script>
96
+
97
+<style lang="scss" scoped>
98
+@import '@/styles/morandi.scss';
99
+@import '@/styles/tab-page.scss';
100
+
101
+.course-detail-page {
102
+  display: flex;
103
+  flex-direction: column;
104
+  min-width: 0;
105
+  min-height: 100%;
106
+  box-sizing: border-box;
107
+  background: $morandi-bg-page;
108
+}
109
+
110
+.course-detail-scroll {
111
+  flex: 1;
112
+  min-height: 0;
113
+  min-width: 0;
114
+}
115
+
116
+.course-detail-inner {
117
+  display: flex;
118
+  flex-direction: column;
119
+  align-items: stretch;
120
+  gap: 28rpx;
121
+  min-width: 0;
122
+  padding: 24rpx 24rpx 48rpx;
123
+  box-sizing: border-box;
124
+}
125
+
126
+.course-detail-top {
127
+  width: 100%;
128
+  min-width: 0;
129
+  border-radius: 16rpx;
130
+  overflow: hidden;
131
+  background: #000;
132
+  border: 1rpx solid $morandi-border;
133
+}
134
+
135
+.course-detail-video {
136
+  display: block;
137
+  width: 100%;
138
+  height: 400rpx;
139
+  background: #1a1a1a;
140
+}
141
+
142
+.course-detail-video--empty {
143
+  display: flex;
144
+  align-items: center;
145
+  justify-content: center;
146
+}
147
+
148
+.course-detail-video__ph {
149
+  color: $morandi-text-muted;
150
+  text-align: center;
151
+  padding: 24rpx;
152
+}
153
+
154
+.course-detail-title {
155
+  display: block;
156
+  width: 100%;
157
+  text-align: center;
158
+  font-size: 36rpx;
159
+  font-weight: 700;
160
+  line-height: 1.45;
161
+  color: #111827;
162
+  word-break: break-word;
163
+  overflow-wrap: anywhere;
164
+}
165
+
166
+.course-detail-bottom {
167
+  display: flex;
168
+  flex-direction: column;
169
+  gap: 16rpx;
170
+  min-width: 0;
171
+  padding: 0 8rpx;
172
+  box-sizing: border-box;
173
+}
174
+
175
+.course-detail-date {
176
+  font-size: 24rpx;
177
+  line-height: 1.5;
178
+  color: $morandi-text-muted;
179
+  text-align: center;
180
+}
181
+
182
+.course-detail-intro {
183
+  font-size: 28rpx;
184
+  line-height: 1.55;
185
+  color: $morandi-text-secondary;
186
+  text-align: left;
187
+  word-break: break-word;
188
+  overflow-wrap: anywhere;
189
+}
190
+
191
+.course-detail-page.lang-bo {
192
+  .course-detail-title {
193
+    font-size: 32rpx;
194
+    line-height: 1.75;
195
+    letter-spacing: 2rpx;
196
+    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
197
+  }
198
+
199
+  .course-detail-date {
200
+    font-size: 22rpx;
201
+    line-height: 1.75;
202
+    letter-spacing: 2rpx;
203
+    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
204
+  }
205
+
206
+  .course-detail-intro {
207
+    font-size: 26rpx;
208
+    line-height: 1.75;
209
+    letter-spacing: 2rpx;
210
+    font-family: 'Noto Sans Tibetan', 'PingFang SC', 'Microsoft YaHei', sans-serif;
211
+  }
212
+}
213
+</style>
214
+

+ 4 - 3
ruoyi-ui-app/package-a/training-detail/index.vue

@@ -45,7 +45,6 @@
45
 import UButton from 'uview-plus/components/u-button/u-button.vue'
45
 import UButton from 'uview-plus/components/u-button/u-button.vue'
46
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
46
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
47
 import tabPage from '@/mixins/tabPage'
47
 import tabPage from '@/mixins/tabPage'
48
-import { syncTabBarText } from '@/utils/tabBar'
49
 import { joinApiUrl } from '@/config'
48
 import { joinApiUrl } from '@/config'
50
 import {
49
 import {
51
   getMyEnrollDetail,
50
   getMyEnrollDetail,
@@ -219,10 +218,12 @@ export default {
219
     this.applyQueryDates(q)
218
     this.applyQueryDates(q)
220
   },
219
   },
221
   onShow() {
220
   onShow() {
222
-    syncTabBarText((k) => this.$t(k))
223
-    uni.setNavigationBarTitle({
221
+    const p = uni.setNavigationBarTitle({
224
       title: this.displayTitleRaw || this.$t(this.navTitleKey)
222
       title: this.displayTitleRaw || this.$t(this.navTitleKey)
225
     })
223
     })
224
+    if (p && typeof p.catch === 'function') {
225
+      p.catch(() => {})
226
+    }
226
     this.loadEnrollDetail()
227
     this.loadEnrollDetail()
227
   },
228
   },
228
   methods: {
229
   methods: {

+ 13 - 1
ruoyi-ui-app/pages/mine/index.vue

@@ -169,9 +169,12 @@ export default {
169
       this.layoutKey += 1
169
       this.layoutKey += 1
170
       this.$nextTick(() => {
170
       this.$nextTick(() => {
171
         syncTabBarText((k) => this.$t(k))
171
         syncTabBarText((k) => this.$t(k))
172
-        uni.setNavigationBarTitle({
172
+        const p = uni.setNavigationBarTitle({
173
           title: this.$t(this.navTitleKey)
173
           title: this.$t(this.navTitleKey)
174
         })
174
         })
175
+        if (p && typeof p.catch === 'function') {
176
+          p.catch(() => {})
177
+        }
175
       })
178
       })
176
     }
179
     }
177
   }
180
   }
@@ -189,6 +192,11 @@ export default {
189
   min-height: 100%;
192
   min-height: 100%;
190
   background: $morandi-bg-page;
193
   background: $morandi-bg-page;
191
   box-sizing: border-box;
194
   box-sizing: border-box;
195
+  /* #ifdef H5 */
196
+  height: 100vh;
197
+  height: 100dvh;
198
+  overflow: hidden;
199
+  /* #endif */
192
 }
200
 }
193
 
201
 
194
 .mine-scroll {
202
 .mine-scroll {
@@ -198,6 +206,10 @@ export default {
198
   min-width: 0;
206
   min-width: 0;
199
   padding: 24rpx 24rpx 40rpx;
207
   padding: 24rpx 24rpx 40rpx;
200
   box-sizing: border-box;
208
   box-sizing: border-box;
209
+  /* #ifdef H5 */
210
+  height: 100%;
211
+  min-height: 1px;
212
+  /* #endif */
201
 }
213
 }
202
 
214
 
203
 .mine-profile {
215
 .mine-profile {

+ 23 - 10
ruoyi-ui-app/uni.promisify.adaptor.js

@@ -1,13 +1,26 @@
1
+function isTabBarFail(err) {
2
+  const msg = err && err.errMsg ? String(err.errMsg) : ''
3
+  return msg.includes('TabBar') && msg.includes('fail')
4
+}
5
+
1
 uni.addInterceptor({
6
 uni.addInterceptor({
2
-  returnValue (res) {
3
-    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
4
-      return res;
7
+  returnValue(res) {
8
+    if (!(!!res && (typeof res === 'object' || typeof res === 'function') && typeof res.then === 'function')) {
9
+      return res
5
     }
10
     }
6
     return new Promise((resolve, reject) => {
11
     return new Promise((resolve, reject) => {
7
-      res.then((res) => {
8
-        if (!res) return resolve(res) 
9
-        return res[0] ? reject(res[0]) : resolve(res[1])
10
-      });
11
-    });
12
-  },
13
-});
12
+      res.then((tuple) => {
13
+        if (!tuple) {
14
+          return resolve(tuple)
15
+        }
16
+        if (tuple[0]) {
17
+          if (isTabBarFail(tuple[0])) {
18
+            return resolve(tuple[0])
19
+          }
20
+          return reject(tuple[0])
21
+        }
22
+        return resolve(tuple[1])
23
+      })
24
+    })
25
+  }
26
+})

+ 61 - 9
ruoyi-ui-app/utils/tabBar.js

@@ -8,30 +8,82 @@ const TAB_PAGE_PATHS = new Set([
8
   'pages/mine/index'
8
   'pages/mine/index'
9
 ])
9
 ])
10
 
10
 
11
+/** 运行时会改 TabBar 的 uni API(非 Tab 页调用会报 not TabBar page) */
12
+const TABBAR_API_NAMES = [
13
+  'setTabBarStyle',
14
+  'setTabBarItem',
15
+  'setTabBarBadge',
16
+  'removeTabBarBadge',
17
+  'showTabBarRedDot',
18
+  'hideTabBarRedDot',
19
+  'showTabBar',
20
+  'hideTabBar',
21
+  'setTabBarMidButton'
22
+]
23
+
24
+function normalizePageRoute(route) {
25
+  if (!route) {
26
+    return ''
27
+  }
28
+  let r = String(route).replace(/^\//, '')
29
+  const prefixes = ['bqH5/', 'bqjy/']
30
+  for (const prefix of prefixes) {
31
+    if (r.startsWith(prefix)) {
32
+      r = r.slice(prefix.length)
33
+    }
34
+  }
35
+  return r
36
+}
37
+
11
 /**
38
 /**
12
- * 当前栈顶是否为原生 TabBar 页;分包等页面调用 setTabBar* 会报 not TabBar page。
39
+ * 当前栈顶是否为原生 TabBar 页
13
  */
40
  */
14
 export function isTabBarPage() {
41
 export function isTabBarPage() {
15
   const stack = getCurrentPages()
42
   const stack = getCurrentPages()
16
-  if (!stack.length) return false
43
+  if (!stack.length) {
44
+    return false
45
+  }
17
   const cur = stack[stack.length - 1]
46
   const cur = stack[stack.length - 1]
18
-  const route = cur.route || ''
47
+  const route = normalizePageRoute(cur.route || '')
19
   return TAB_PAGE_PATHS.has(route)
48
   return TAB_PAGE_PATHS.has(route)
20
 }
49
 }
21
 
50
 
51
+function installTabBarApiGuard() {
52
+  if (typeof uni === 'undefined' || !uni.addInterceptor) {
53
+    return
54
+  }
55
+  TABBAR_API_NAMES.forEach((name) => {
56
+    if (typeof uni[name] !== 'function') {
57
+      return
58
+    }
59
+    uni.addInterceptor(name, {
60
+      invoke() {
61
+        if (!isTabBarPage()) {
62
+          return false
63
+        }
64
+      }
65
+    })
66
+  })
67
+}
68
+
69
+installTabBarApiGuard()
70
+
22
 /**
71
 /**
23
- * 原生 tabBar 的 text 不能写进 pages.json 的 i18n,需在运行时按当前语言更新。
72
+ * 原生 tabBar 的 text 不能写进 pages.json 的 i18n,需在运行时按当前语言更新(仅 Tab 页)
24
  * @param {(key: string) => string} t  如 (k) => this.$t(k)
73
  * @param {(key: string) => string} t  如 (k) => this.$t(k)
25
  */
74
  */
26
 export function syncTabBarText(t) {
75
 export function syncTabBarText(t) {
27
-  if (!isTabBarPage()) return
76
+  if (!isTabBarPage()) {
77
+    return
78
+  }
28
   TAB_KEYS.forEach((key, index) => {
79
   TAB_KEYS.forEach((key, index) => {
29
-    uni.setTabBarItem({
80
+    const p = uni.setTabBarItem({
30
       index,
81
       index,
31
       text: t(key),
82
       text: t(key),
32
-      fail() {
33
-        /* 个别端仍可能失败,静默即可 */
34
-      }
83
+      fail() {}
35
     })
84
     })
85
+    if (p && typeof p.catch === 'function') {
86
+      p.catch(() => {})
87
+    }
36
   })
88
   })
37
 }
89
 }

+ 18 - 0
ruoyi-ui-app/vite.config.js

@@ -4,6 +4,18 @@ import { LLM_PROXY_TARGET } from './config/llm.js'
4
 
4
 
5
 const llmTarget = String(LLM_PROXY_TARGET || '').replace(/\/$/, '')
5
 const llmTarget = String(LLM_PROXY_TARGET || '').replace(/\/$/, '')
6
 
6
 
7
+/** 与 manifest.json h5.router.base 一致:生产 /bqH5/,开发 / */
8
+function resolvePublicPath(mode, env) {
9
+  if (mode !== 'production') {
10
+    return '/'
11
+  }
12
+  const raw = String(env.VITE_APP_PUBLIC_PATH || 'bqH5').trim()
13
+  if (!raw || raw === '/') {
14
+    return '/'
15
+  }
16
+  return `/${raw.replace(/^\/+|\/+$/g, '')}/`
17
+}
18
+
7
 /**
19
 /**
8
  * uni-app Vue3 H5 开发代理
20
  * uni-app Vue3 H5 开发代理
9
  * - /dev-api → 若依后端(.env.development 的 VITE_APP_API_HOST)
21
  * - /dev-api → 若依后端(.env.development 的 VITE_APP_API_HOST)
@@ -11,11 +23,17 @@ const llmTarget = String(LLM_PROXY_TARGET || '').replace(/\/$/, '')
11
  */
23
  */
12
 export default defineConfig(({ mode }) => {
24
 export default defineConfig(({ mode }) => {
13
   const env = loadEnv(mode, process.cwd(), '')
25
   const env = loadEnv(mode, process.cwd(), '')
26
+  const base = resolvePublicPath(mode, env)
14
   const devApiTarget = String(env.VITE_APP_API_HOST || 'http://192.168.1.6:8010').replace(/\/$/, '')
27
   const devApiTarget = String(env.VITE_APP_API_HOST || 'http://192.168.1.6:8010').replace(/\/$/, '')
15
   const devApiPrefix = String(env.VITE_APP_BASE_API || '/dev-api')
28
   const devApiPrefix = String(env.VITE_APP_BASE_API || '/dev-api')
16
 
29
 
17
   return {
30
   return {
31
+    base,
18
     plugins: [uni()],
32
     plugins: [uni()],
33
+    build: {
34
+      // iOS Safari 对 modulepreload 更敏感,预加载未使用的 chunk 易拖慢首屏
35
+      modulePreload: false
36
+    },
19
     server: {
37
     server: {
20
       proxy: {
38
       proxy: {
21
         [devApiPrefix]: {
39
         [devApiPrefix]: {