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 204
 <style lang="scss" scoped>
205 205
 @import '@/styles/morandi.scss';
206
+@import '@/styles/scroll-fill.scss';
206 207
 
207 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 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 170
 <style lang="scss" scoped>
171 171
 @import '@/styles/morandi.scss';
172
+@import '@/styles/scroll-fill.scss';
172 173
 
173 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 176
   background: $morandi-bg-page;
180 177
 }
181 178
 
@@ -212,10 +209,7 @@ export default {
212 209
 }
213 210
 
214 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 215
 .sp-order-list {

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

@@ -21,12 +21,11 @@ export default {
21 21
 
22 22
 <style lang="scss" scoped>
23 23
 @import '@/styles/morandi.scss';
24
+@import '@/styles/scroll-fill.scss';
24 25
 
25 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 29
   align-items: center;
31 30
   justify-content: center;
32 31
   padding: 48rpx 40rpx;

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

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

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

@@ -224,13 +224,10 @@ export default {
224 224
 
225 225
 <style lang="scss" scoped>
226 226
 @import '@/styles/morandi.scss';
227
+@import '@/styles/scroll-fill.scss';
227 228
 
228 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 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 582
 @import '@/styles/morandi.scss';
583 583
 @import '@/styles/tab-page.scss';
584 584
 @import '@/styles/app-lang.scss';
585
+@import '@/styles/scroll-fill.scss';
585 586
 
586 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 590
   background: $morandi-bg-page;
593 591
 }
594 592
 
595 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 597
 .ym-hero {

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

@@ -177,13 +177,10 @@ export default {
177 177
 
178 178
 <style lang="scss" scoped>
179 179
 @import '@/styles/morandi.scss';
180
+@import '@/styles/scroll-fill.scss';
180 181
 
181 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 184
   background: $morandi-bg-page;
188 185
 }
189 186
 
@@ -220,10 +217,7 @@ export default {
220 217
 }
221 218
 
222 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 223
 .sp-order-list {

+ 2 - 1
ruoyi-jiaoyi/main.js

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

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

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

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

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

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

@@ -66,11 +66,5 @@ export default {
66 66
 </script>
67 67
 
68 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 70
 </style>

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

@@ -44,11 +44,5 @@ export default {
44 44
 </script>
45 45
 
46 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 48
 </style>

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

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

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

@@ -60,11 +60,5 @@ export default {
60 60
 </script>
61 61
 
62 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 64
 </style>

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

@@ -183,15 +183,14 @@ export default {
183 183
 
184 184
 <style lang="scss" scoped>
185 185
 @import '@/styles/morandi.scss';
186
+@import '@/styles/detail-page.scss';
186 187
 
187 188
 .sp-detail-page {
188
-  min-height: 100%;
189
-  background: $morandi-bg-page;
189
+  @include detail-page-root;
190 190
 }
191 191
 
192 192
 .sp-detail-scroll {
193
-  min-height: 100%;
194
-  box-sizing: border-box;
193
+  @include detail-page-scroll;
195 194
 }
196 195
 
197 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 128
   min-width: 0;
129 129
   min-height: 100%;
130 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 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 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 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 2
 VITE_APP_TITLE=掌上巴青
3
+
4
+# 生产环境
5
+ENV=production
6
+
7
+# 若依后端 API(与 ruoyi-ui 一致,由 Nginx 反代到真实后端)
3 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 3
 import App from './App'
2 4
 
3
-
4 5
 // #ifndef VUE3
5 6
 import Vue from 'vue'
6
-import './uni.promisify.adaptor'
7 7
 Vue.config.productionTip = false
8 8
 App.mpType = 'app'
9 9
 const app = new Vue({

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

@@ -22,9 +22,12 @@ export default {
22 22
     syncTabBarText((k) => this.$t(k))
23 23
     const key = this.navTitleKey
24 24
     if (key) {
25
-      uni.setNavigationBarTitle({
25
+      const p = uni.setNavigationBarTitle({
26 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 166
 import UButton from 'uview-plus/components/u-button/u-button.vue'
167 167
 import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
168 168
 import tabPage from '@/mixins/tabPage'
169
-import { syncTabBarText } from '@/utils/tabBar'
170 169
 import { ensureApiToken } from '@/utils/apiAuth'
171 170
 import { uploadFile } from '@/utils/upload'
172 171
 import {
@@ -278,10 +277,12 @@ export default {
278 277
     setTimeout(() => uni.navigateBack(), 1500)
279 278
   },
280 279
   onShow() {
281
-    syncTabBarText((k) => this.$t(k))
282
-    uni.setNavigationBarTitle({
280
+    const p = uni.setNavigationBarTitle({
283 281
       title: this.peerName || this.$t(this.navTitleKey)
284 282
     })
283
+    if (p && typeof p.catch === 'function') {
284
+      p.catch(() => {})
285
+    }
285 286
     if (this.sessionReady) {
286 287
       this.pollNewMessages()
287 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 45
 import UButton from 'uview-plus/components/u-button/u-button.vue'
46 46
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
47 47
 import tabPage from '@/mixins/tabPage'
48
-import { syncTabBarText } from '@/utils/tabBar'
49 48
 import { joinApiUrl } from '@/config'
50 49
 import {
51 50
   getMyEnrollDetail,
@@ -219,10 +218,12 @@ export default {
219 218
     this.applyQueryDates(q)
220 219
   },
221 220
   onShow() {
222
-    syncTabBarText((k) => this.$t(k))
223
-    uni.setNavigationBarTitle({
221
+    const p = uni.setNavigationBarTitle({
224 222
       title: this.displayTitleRaw || this.$t(this.navTitleKey)
225 223
     })
224
+    if (p && typeof p.catch === 'function') {
225
+      p.catch(() => {})
226
+    }
226 227
     this.loadEnrollDetail()
227 228
   },
228 229
   methods: {

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

@@ -169,9 +169,12 @@ export default {
169 169
       this.layoutKey += 1
170 170
       this.$nextTick(() => {
171 171
         syncTabBarText((k) => this.$t(k))
172
-        uni.setNavigationBarTitle({
172
+        const p = uni.setNavigationBarTitle({
173 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 192
   min-height: 100%;
190 193
   background: $morandi-bg-page;
191 194
   box-sizing: border-box;
195
+  /* #ifdef H5 */
196
+  height: 100vh;
197
+  height: 100dvh;
198
+  overflow: hidden;
199
+  /* #endif */
192 200
 }
193 201
 
194 202
 .mine-scroll {
@@ -198,6 +206,10 @@ export default {
198 206
   min-width: 0;
199 207
   padding: 24rpx 24rpx 40rpx;
200 208
   box-sizing: border-box;
209
+  /* #ifdef H5 */
210
+  height: 100%;
211
+  min-height: 1px;
212
+  /* #endif */
201 213
 }
202 214
 
203 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 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 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 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 41
 export function isTabBarPage() {
15 42
   const stack = getCurrentPages()
16
-  if (!stack.length) return false
43
+  if (!stack.length) {
44
+    return false
45
+  }
17 46
   const cur = stack[stack.length - 1]
18
-  const route = cur.route || ''
47
+  const route = normalizePageRoute(cur.route || '')
19 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 73
  * @param {(key: string) => string} t  如 (k) => this.$t(k)
25 74
  */
26 75
 export function syncTabBarText(t) {
27
-  if (!isTabBarPage()) return
76
+  if (!isTabBarPage()) {
77
+    return
78
+  }
28 79
   TAB_KEYS.forEach((key, index) => {
29
-    uni.setTabBarItem({
80
+    const p = uni.setTabBarItem({
30 81
       index,
31 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 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 20
  * uni-app Vue3 H5 开发代理
9 21
  * - /dev-api → 若依后端(.env.development 的 VITE_APP_API_HOST)
@@ -11,11 +23,17 @@ const llmTarget = String(LLM_PROXY_TARGET || '').replace(/\/$/, '')
11 23
  */
12 24
 export default defineConfig(({ mode }) => {
13 25
   const env = loadEnv(mode, process.cwd(), '')
26
+  const base = resolvePublicPath(mode, env)
14 27
   const devApiTarget = String(env.VITE_APP_API_HOST || 'http://192.168.1.6:8010').replace(/\/$/, '')
15 28
   const devApiPrefix = String(env.VITE_APP_BASE_API || '/dev-api')
16 29
 
17 30
   return {
31
+    base,
18 32
     plugins: [uni()],
33
+    build: {
34
+      // iOS Safari 对 modulepreload 更敏感,预加载未使用的 chunk 易拖慢首屏
35
+      modulePreload: false
36
+    },
19 37
     server: {
20 38
       proxy: {
21 39
         [devApiPrefix]: {