xsh_1997 vor 1 Woche
Ursprung
Commit
359f867044
29 geänderte Dateien mit 359 neuen und 84 gelöschten Zeilen
  1. 80 0
      ruoyi-jiaoyi/components/ImagePreview/index.vue
  2. 25 2
      ruoyi-jiaoyi/config/index.js
  3. 3 7
      ruoyi-jiaoyi/pages/distributor/order-detail/index.vue
  4. 2 4
      ruoyi-jiaoyi/pages/distributor/yak-detail/index.vue
  5. 5 8
      ruoyi-jiaoyi/pages/supplier/order-detail/index.vue
  6. 2 12
      ruoyi-jiaoyi/store/user.js
  7. 38 0
      ruoyi-jiaoyi/utils/resourceUrl.js
  8. 12 0
      ruoyi-jiaoyi/utils/validate.js
  9. 80 0
      ruoyi-ui-app/components/ImagePreview/index.vue
  10. 25 2
      ruoyi-ui-app/config/index.js
  11. 2 2
      ruoyi-ui-app/package-a/agri-classroom/index.vue
  12. 2 2
      ruoyi-ui-app/package-a/booking-expert/index.vue
  13. 2 2
      ruoyi-ui-app/package-a/booking-org/index.vue
  14. 2 2
      ruoyi-ui-app/package-a/booking-service/index.vue
  15. 2 2
      ruoyi-ui-app/package-a/booking-vet/index.vue
  16. 2 2
      ruoyi-ui-app/package-a/breeding-news/index.vue
  17. 3 3
      ruoyi-ui-app/package-a/course-detail/index.vue
  18. 2 2
      ruoyi-ui-app/package-a/livestock-resources/index.vue
  19. 2 2
      ruoyi-ui-app/package-a/my-enrollment/index.vue
  20. 3 3
      ruoyi-ui-app/package-a/news-detail/index.vue
  21. 2 2
      ruoyi-ui-app/package-a/online-clinic/index.vue
  22. 3 3
      ruoyi-ui-app/package-a/training-detail/index.vue
  23. 2 2
      ruoyi-ui-app/package-a/vet-profile/index.vue
  24. 2 2
      ruoyi-ui-app/pages/message/index.vue
  25. 2 12
      ruoyi-ui-app/store/user.js
  26. 2 4
      ruoyi-ui-app/utils/aiConsult.js
  27. 2 2
      ruoyi-ui-app/utils/filePreview.js
  28. 38 0
      ruoyi-ui-app/utils/resourceUrl.js
  29. 12 0
      ruoyi-ui-app/utils/validate.js

+ 80 - 0
ruoyi-jiaoyi/components/ImagePreview/index.vue

@@ -0,0 +1,80 @@
1
+<template>
2
+  <image
3
+    v-if="realSrc"
4
+    :src="realSrc"
5
+    :mode="mode"
6
+    :class="imgClass"
7
+    :style="imgStyle"
8
+    @click="onImageClick"
9
+    @error="$emit('error', $event)"
10
+  />
11
+</template>
12
+
13
+<script>
14
+import { resolveResourceUrl, resolveResourceUrlList } from '@/utils/resourceUrl'
15
+
16
+/**
17
+ * 接口图片预览(对齐 ruoyi-ui/src/components/ImagePreview)
18
+ * 相对路径自动拼接 BASE_API;H5 生产环境为当前浏览器 origin + /prod-api
19
+ */
20
+export default {
21
+  name: 'ImagePreview',
22
+  props: {
23
+    src: {
24
+      type: String,
25
+      default: ''
26
+    },
27
+    mode: {
28
+      type: String,
29
+      default: 'aspectFill'
30
+    },
31
+    width: {
32
+      type: [Number, String],
33
+      default: ''
34
+    },
35
+    height: {
36
+      type: [Number, String],
37
+      default: ''
38
+    },
39
+    preview: {
40
+      type: Boolean,
41
+      default: false
42
+    },
43
+    imgClass: {
44
+      type: String,
45
+      default: ''
46
+    }
47
+  },
48
+  emits: ['error', 'click'],
49
+  computed: {
50
+    realSrc() {
51
+      return resolveResourceUrl(this.src)
52
+    },
53
+    realSrcList() {
54
+      return resolveResourceUrlList(this.src)
55
+    },
56
+    imgStyle() {
57
+      const style = {}
58
+      if (this.width !== '' && this.width != null) {
59
+        style.width = typeof this.width === 'string' ? this.width : `${this.width}px`
60
+      }
61
+      if (this.height !== '' && this.height != null) {
62
+        style.height = typeof this.height === 'string' ? this.height : `${this.height}px`
63
+      }
64
+      return style
65
+    }
66
+  },
67
+  methods: {
68
+    onImageClick(e) {
69
+      this.$emit('click', e)
70
+      if (!this.preview || !this.realSrcList.length) {
71
+        return
72
+      }
73
+      uni.previewImage({
74
+        urls: this.realSrcList,
75
+        current: this.realSrc
76
+      })
77
+    }
78
+  }
79
+}
80
+</script>

+ 25 - 2
ruoyi-jiaoyi/config/index.js

@@ -7,11 +7,21 @@
7 7
  * 打包 H5:uni build -p h5
8 8
  */
9 9
 
10
+import { isExternal } from '@/utils/validate'
11
+
10 12
 const apiHost = String(import.meta.env.VITE_APP_API_HOST || 'http://192.168.1.6:8010').replace(/\/$/, '')
11 13
 
12 14
 /** 与 ruoyi-ui 的 VUE_APP_BASE_API 同源配置(dev/prod 由 .env 注入) */
13 15
 const envBaseApi = String(import.meta.env.VITE_APP_BASE_API || '').trim()
14 16
 
17
+/** H5 打包后取浏览器当前 host(IP/域名),与 ruoyi-ui 生产 /prod-api 同域反代一致 */
18
+function getBrowserOrigin() {
19
+  if (typeof window === 'undefined' || !window.location) {
20
+    return ''
21
+  }
22
+  return String(window.location.origin || '').replace(/\/$/, '')
23
+}
24
+
15 25
 /**
16 26
  * H5 开发是否走 /dev-api 代理(需 vite.config.js rewrite)
17 27
  * 默认 false:直连 VITE_APP_API_HOST
@@ -20,7 +30,20 @@ export const H5_USE_PROXY = false
20 30
 
21 31
 function resolveBaseApi() {
22 32
   if (import.meta.env.PROD) {
23
-    return envBaseApi || '/prod-api'
33
+    const apiPath = envBaseApi || '/prod-api'
34
+    // #ifdef H5
35
+    const origin = getBrowserOrigin()
36
+    if (origin) {
37
+      const normalized = apiPath.startsWith('/') ? apiPath : `/${apiPath}`
38
+      return `${origin}${normalized}`.replace(/\/$/, '')
39
+    }
40
+    // #endif
41
+    // #ifndef H5
42
+    if (/^https?:\/\//i.test(apiHost)) {
43
+      return apiHost
44
+    }
45
+    // #endif
46
+    return apiPath
24 47
   }
25 48
   const devBase = envBaseApi || '/dev-api'
26 49
   // #ifdef H5
@@ -41,7 +64,7 @@ export function joinApiUrl(path) {
41 64
   if (!path) {
42 65
     return BASE_API
43 66
   }
44
-  if (/^https?:\/\//i.test(path)) {
67
+  if (isExternal(path)) {
45 68
     return path
46 69
   }
47 70
   const p = path.startsWith('/') ? path : '/' + path

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

@@ -68,7 +68,7 @@
68 68
 
69 69
 <script>
70 70
 import { getDistributorOrder } from '@/api/distributor/order'
71
-import { joinApiUrl } from '@/config'
71
+import { resolveResourceUrl } from '@/utils/resourceUrl'
72 72
 import { DASH, formatDateTime, formatPrice } from '@/utils/format'
73 73
 
74 74
 export default {
@@ -105,7 +105,8 @@ export default {
105 105
       ]
106 106
     },
107 107
     payVoucherUrl() {
108
-      return this.resolveMediaUrl(this.detail && (this.detail.payVoucherUrl || this.detail.payVoucherPath))
108
+      const d = this.detail
109
+      return resolveResourceUrl(d && (d.payVoucherUrl || d.payVoucherPath))
109 110
     }
110 111
   },
111 112
   onLoad(query) {
@@ -129,11 +130,6 @@ export default {
129 130
           this.loading = false
130 131
         })
131 132
     },
132
-    resolveMediaUrl(path) {
133
-      if (!path) return ''
134
-      if (/^https?:\/\//i.test(path)) return path
135
-      return joinApiUrl(path)
136
-    },
137 133
     previewImage(url) {
138 134
       if (!url) return
139 135
       uni.previewImage({ urls: [url], current: url })

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

@@ -47,7 +47,7 @@
47 47
 
48 48
 <script>
49 49
 import { getDistributorYakBasic } from '@/api/distributor/yak'
50
-import { joinApiUrl } from '@/config'
50
+import { resolveResourceUrl } from '@/utils/resourceUrl'
51 51
 import { DASH, formatDateTime } from '@/utils/format'
52 52
 
53 53
 export default {
@@ -105,9 +105,7 @@ export default {
105 105
     },
106 106
     resolvePhoto(item) {
107 107
       const path = item && (item.photoUrl || item.photoPath)
108
-      if (!path) return ''
109
-      if (/^https?:\/\//i.test(path)) return path
110
-      return joinApiUrl(path)
108
+      return resolveResourceUrl(path)
111 109
     },
112 110
     previewImage(url) {
113 111
       if (!url) return

+ 5 - 8
ruoyi-jiaoyi/pages/supplier/order-detail/index.vue

@@ -95,7 +95,7 @@
95 95
 
96 96
 <script>
97 97
 import { getSupplierOrder, getSupplierSettlement } from '@/api/supplier/order'
98
-import { joinApiUrl } from '@/config'
98
+import { resolveResourceUrl } from '@/utils/resourceUrl'
99 99
 import { DASH, formatDateTime, formatPrice } from '@/utils/format'
100 100
 
101 101
 export default {
@@ -137,10 +137,12 @@ export default {
137 137
       ]
138 138
     },
139 139
     payVoucherUrl() {
140
-      return this.resolveMediaUrl(this.detail && (this.detail.payVoucherUrl || this.detail.payVoucherPath))
140
+      const d = this.detail
141
+      return resolveResourceUrl(d && (d.payVoucherUrl || d.payVoucherPath))
141 142
     },
142 143
     settleVoucherUrl() {
143
-      return this.resolveMediaUrl(this.detail && (this.detail.settleVoucherUrl || this.detail.settleVoucherPath))
144
+      const d = this.detail
145
+      return resolveResourceUrl(d && (d.settleVoucherUrl || d.settleVoucherPath))
144 146
     }
145 147
   },
146 148
   onLoad(query) {
@@ -168,11 +170,6 @@ export default {
168 170
           this.loading = false
169 171
         })
170 172
     },
171
-    resolveMediaUrl(path) {
172
-      if (!path) return ''
173
-      if (/^https?:\/\//i.test(path)) return path
174
-      return joinApiUrl(path)
175
-    },
176 173
     previewImage(url) {
177 174
       if (!url) return
178 175
       uni.previewImage({ urls: [url], current: url })

+ 2 - 12
ruoyi-jiaoyi/store/user.js

@@ -1,7 +1,7 @@
1 1
 import { reactive } from 'vue'
2 2
 import { login as loginApi, logout as logoutApi, getInfo } from '@/api/login'
3 3
 import { getToken, setToken, removeToken } from '@/utils/auth'
4
-import { joinApiUrl } from '@/config'
4
+import { resolveResourceUrl } from '@/utils/resourceUrl'
5 5
 import { hasPartnerRole, normalizeRoles, PARTNER_ROLE_DENIED_MSG, shouldDenyPartnerLogin } from '@/utils/partnerRole'
6 6
 
7 7
 const state = reactive({
@@ -15,18 +15,8 @@ const state = reactive({
15 15
   permissions: []
16 16
 })
17 17
 
18
-function isHttp(url) {
19
-  return /^https?:\/\//i.test(url || '')
20
-}
21
-
22 18
 function resolveAvatar(path) {
23
-  if (!path) {
24
-    return ''
25
-  }
26
-  if (isHttp(path)) {
27
-    return path
28
-  }
29
-  return joinApiUrl(path)
19
+  return resolveResourceUrl(path)
30 20
 }
31 21
 
32 22
 function clearSessionState() {

+ 38 - 0
ruoyi-jiaoyi/utils/resourceUrl.js

@@ -0,0 +1,38 @@
1
+import { joinApiUrl } from '@/config'
2
+import { isExternal } from '@/utils/validate'
3
+
4
+/**
5
+ * 将接口返回的资源路径转为可访问 URL(对齐 ruoyi-ui ImagePreview)
6
+ * - 外链 / data: / blob: 原样返回
7
+ * - 相对路径拼接 BASE_API(H5 生产为当前浏览器 origin + /prod-api)
8
+ * @param {string} src 支持逗号分隔时取第一项
9
+ */
10
+export function resolveResourceUrl(src) {
11
+  if (!src) {
12
+    return ''
13
+  }
14
+  const path = String(src).split(',')[0].trim()
15
+  if (!path) {
16
+    return ''
17
+  }
18
+  if (isExternal(path)) {
19
+    return path
20
+  }
21
+  return joinApiUrl(path)
22
+}
23
+
24
+/**
25
+ * 多图预览列表(对齐 ImagePreview realSrcList)
26
+ * @param {string} src 逗号分隔
27
+ * @returns {string[]}
28
+ */
29
+export function resolveResourceUrlList(src) {
30
+  if (!src) {
31
+    return []
32
+  }
33
+  return String(src)
34
+    .split(',')
35
+    .map((item) => item.trim())
36
+    .filter(Boolean)
37
+    .map((item) => (isExternal(item) ? item : joinApiUrl(item)))
38
+}

+ 12 - 0
ruoyi-jiaoyi/utils/validate.js

@@ -0,0 +1,12 @@
1
+/**
2
+ * 判断 path 是否为外链(对齐 ruoyi-ui/src/utils/validate.js)
3
+ * @param {string} path
4
+ */
5
+export function isExternal(path) {
6
+  return /^(https?:|mailto:|tel:|data:|blob:)/.test(path || '')
7
+}
8
+
9
+/** 是否为 http(s) 绝对地址 */
10
+export function isHttp(url) {
11
+  return /^https?:\/\//i.test(url || '')
12
+}

+ 80 - 0
ruoyi-ui-app/components/ImagePreview/index.vue

@@ -0,0 +1,80 @@
1
+<template>
2
+  <image
3
+    v-if="realSrc"
4
+    :src="realSrc"
5
+    :mode="mode"
6
+    :class="imgClass"
7
+    :style="imgStyle"
8
+    @click="onImageClick"
9
+    @error="$emit('error', $event)"
10
+  />
11
+</template>
12
+
13
+<script>
14
+import { resolveResourceUrl, resolveResourceUrlList } from '@/utils/resourceUrl'
15
+
16
+/**
17
+ * 接口图片预览(对齐 ruoyi-ui/src/components/ImagePreview)
18
+ * 相对路径自动拼接 BASE_API;H5 生产环境为当前浏览器 origin + /prod-api
19
+ */
20
+export default {
21
+  name: 'ImagePreview',
22
+  props: {
23
+    src: {
24
+      type: String,
25
+      default: ''
26
+    },
27
+    mode: {
28
+      type: String,
29
+      default: 'aspectFill'
30
+    },
31
+    width: {
32
+      type: [Number, String],
33
+      default: ''
34
+    },
35
+    height: {
36
+      type: [Number, String],
37
+      default: ''
38
+    },
39
+    preview: {
40
+      type: Boolean,
41
+      default: false
42
+    },
43
+    imgClass: {
44
+      type: String,
45
+      default: ''
46
+    }
47
+  },
48
+  emits: ['error', 'click'],
49
+  computed: {
50
+    realSrc() {
51
+      return resolveResourceUrl(this.src)
52
+    },
53
+    realSrcList() {
54
+      return resolveResourceUrlList(this.src)
55
+    },
56
+    imgStyle() {
57
+      const style = {}
58
+      if (this.width !== '' && this.width != null) {
59
+        style.width = typeof this.width === 'string' ? this.width : `${this.width}px`
60
+      }
61
+      if (this.height !== '' && this.height != null) {
62
+        style.height = typeof this.height === 'string' ? this.height : `${this.height}px`
63
+      }
64
+      return style
65
+    }
66
+  },
67
+  methods: {
68
+    onImageClick(e) {
69
+      this.$emit('click', e)
70
+      if (!this.preview || !this.realSrcList.length) {
71
+        return
72
+      }
73
+      uni.previewImage({
74
+        urls: this.realSrcList,
75
+        current: this.realSrc
76
+      })
77
+    }
78
+  }
79
+}
80
+</script>

+ 25 - 2
ruoyi-ui-app/config/index.js

@@ -7,11 +7,21 @@
7 7
  * 打包 H5:uni build -p h5
8 8
  */
9 9
 
10
+import { isExternal } from '@/utils/validate'
11
+
10 12
 const apiHost = String(import.meta.env.VITE_APP_API_HOST || 'http://192.168.1.6:8010').replace(/\/$/, '')
11 13
 
12 14
 /** 与 ruoyi-ui 的 VUE_APP_BASE_API 同源配置(dev/prod 由 .env 注入) */
13 15
 const envBaseApi = String(import.meta.env.VITE_APP_BASE_API || '').trim()
14 16
 
17
+/** H5 打包后取浏览器当前 host(IP/域名),与 ruoyi-ui 生产 /prod-api 同域反代一致 */
18
+function getBrowserOrigin() {
19
+  if (typeof window === 'undefined' || !window.location) {
20
+    return ''
21
+  }
22
+  return String(window.location.origin || '').replace(/\/$/, '')
23
+}
24
+
15 25
 /**
16 26
  * H5 开发是否走 /dev-api 代理(需 vite.config.js rewrite)
17 27
  * 默认 false:直连 VITE_APP_API_HOST
@@ -20,7 +30,20 @@ export const H5_USE_PROXY = false
20 30
 
21 31
 function resolveBaseApi() {
22 32
   if (import.meta.env.PROD) {
23
-    return envBaseApi || '/prod-api'
33
+    const apiPath = envBaseApi || '/prod-api'
34
+    // #ifdef H5
35
+    const origin = getBrowserOrigin()
36
+    if (origin) {
37
+      const normalized = apiPath.startsWith('/') ? apiPath : `/${apiPath}`
38
+      return `${origin}${normalized}`.replace(/\/$/, '')
39
+    }
40
+    // #endif
41
+    // #ifndef H5
42
+    if (/^https?:\/\//i.test(apiHost)) {
43
+      return apiHost
44
+    }
45
+    // #endif
46
+    return apiPath
24 47
   }
25 48
   const devBase = envBaseApi || '/dev-api'
26 49
   // #ifdef H5
@@ -41,7 +64,7 @@ export function joinApiUrl(path) {
41 64
   if (!path) {
42 65
     return BASE_API
43 66
   }
44
-  if (/^https?:\/\//i.test(path)) {
67
+  if (isExternal(path)) {
45 68
     return path
46 69
   }
47 70
   const p = path.startsWith('/') ? path : '/' + path

+ 2 - 2
ruoyi-ui-app/package-a/agri-classroom/index.vue

@@ -128,7 +128,7 @@ import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
128 128
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
129 129
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
130 130
 import tabPage from '@/mixins/tabPage'
131
-import { joinApiUrl } from '@/config'
131
+import { resolveResourceUrl } from '@/utils/resourceUrl'
132 132
 import { listAgriculturalCourse } from '@/api/agriculturalCourse'
133 133
 
134 134
 /** 视频课程 course_topic(005001–005004) */
@@ -450,7 +450,7 @@ export default {
450 450
     },
451 451
     articleCover(card) {
452 452
       if (card.coverFileUrl) {
453
-        return joinApiUrl(card.coverFileUrl)
453
+        return resolveResourceUrl(card.coverFileUrl)
454 454
       }
455 455
       return this.coverSrc
456 456
     },

+ 2 - 2
ruoyi-ui-app/package-a/booking-expert/index.vue

@@ -203,7 +203,7 @@ import UPopup from 'uview-plus/components/u-popup/u-popup.vue'
203 203
 import URate from 'uview-plus/components/u-rate/u-rate.vue'
204 204
 import UTextarea from 'uview-plus/components/u-textarea/u-textarea.vue'
205 205
 import tabPage from '@/mixins/tabPage'
206
-import { joinApiUrl } from '@/config'
206
+import { resolveResourceUrl } from '@/utils/resourceUrl'
207 207
 import { ensureApiToken } from '@/utils/apiAuth'
208 208
 import { useUserStore } from '@/store/user'
209 209
 import {
@@ -347,7 +347,7 @@ export default {
347 347
     },
348 348
     photoSrc() {
349 349
       const url = this.resource && this.resource.photoFileUrl
350
-      return url ? joinApiUrl(url) : ''
350
+      return url ? resolveResourceUrl(url) : ''
351 351
     },
352 352
     selectedDateBooked() {
353 353
       return !!this.bookedDateMap[this.selectedDateKey]

+ 2 - 2
ruoyi-ui-app/package-a/booking-org/index.vue

@@ -132,7 +132,7 @@ import UFormItem from 'uview-plus/components/u-form-item/u-form-item.vue'
132 132
 import UInput from 'uview-plus/components/u-input/u-input.vue'
133 133
 import UPopup from 'uview-plus/components/u-popup/u-popup.vue'
134 134
 import tabPage from '@/mixins/tabPage'
135
-import { joinApiUrl } from '@/config'
135
+import { resolveResourceUrl } from '@/utils/resourceUrl'
136 136
 import { ensureApiToken } from '@/utils/apiAuth'
137 137
 import { useUserStore } from '@/store/user'
138 138
 import {
@@ -213,7 +213,7 @@ export default {
213 213
     },
214 214
     photoSrc() {
215 215
       const url = this.resource && this.resource.photoFileUrl
216
-      return url ? joinApiUrl(url) : ''
216
+      return url ? resolveResourceUrl(url) : ''
217 217
     },
218 218
     selectedDateBooked() {
219 219
       return !!this.bookedDateMap[this.selectedDateKey]

+ 2 - 2
ruoyi-ui-app/package-a/booking-service/index.vue

@@ -115,7 +115,7 @@ import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
115 115
 import UButton from 'uview-plus/components/u-button/u-button.vue'
116 116
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
117 117
 import tabPage from '@/mixins/tabPage'
118
-import { joinApiUrl } from '@/config'
118
+import { resolveResourceUrl } from '@/utils/resourceUrl'
119 119
 import { ensureApiToken } from '@/utils/apiAuth'
120 120
 import {
121 121
   listBookingDates,
@@ -445,7 +445,7 @@ export default {
445 445
     },
446 446
     avatarUrl(item) {
447 447
       const url = item && (item.photoFileUrl || (item.raw && item.raw.photoFileUrl))
448
-      const result = url ? joinApiUrl(url) : ''
448
+      const result = url ? resolveResourceUrl(url) : ''
449 449
       return result
450 450
     },
451 451
     onBook(item) {

+ 2 - 2
ruoyi-ui-app/package-a/booking-vet/index.vue

@@ -184,7 +184,7 @@ import UPicker from 'uview-plus/components/u-picker/u-picker.vue'
184 184
 import UPopup from 'uview-plus/components/u-popup/u-popup.vue'
185 185
 import UTextarea from 'uview-plus/components/u-textarea/u-textarea.vue'
186 186
 import tabPage from '@/mixins/tabPage'
187
-import { joinApiUrl } from '@/config'
187
+import { resolveResourceUrl } from '@/utils/resourceUrl'
188 188
 import { ensureApiToken } from '@/utils/apiAuth'
189 189
 import { useUserStore } from '@/store/user'
190 190
 import {
@@ -342,7 +342,7 @@ export default {
342 342
     },
343 343
     photoSrc() {
344 344
       const url = this.resource && this.resource.photoFileUrl
345
-      return url ? joinApiUrl(url) : ''
345
+      return url ? resolveResourceUrl(url) : ''
346 346
     },
347 347
     avatarText() {
348 348
       const name = (this.resource && this.resource.resourceName) || ''

+ 2 - 2
ruoyi-ui-app/package-a/breeding-news/index.vue

@@ -127,7 +127,7 @@ import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
127 127
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
128 128
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
129 129
 import tabPage from '@/mixins/tabPage'
130
-import { joinApiUrl } from '@/config'
130
+import { resolveResourceUrl } from '@/utils/resourceUrl'
131 131
 import { putNewsDetailPayload } from '@/utils/newsDetailCache'
132 132
 import { listInformationCategoryTree } from '@/api/category/informationCategory'
133 133
 import { listFarmingNews } from '@/api/farmingNews'
@@ -500,7 +500,7 @@ export default {
500 500
     /** 列表项封面 */
501 501
     articleCover(item) {
502 502
       if (item.coverFileUrl) {
503
-        return joinApiUrl(item.coverFileUrl)
503
+        return resolveResourceUrl(item.coverFileUrl)
504 504
       }
505 505
       return this.coverSrc
506 506
     },

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

@@ -30,7 +30,7 @@
30 30
 
31 31
 <script>
32 32
 import tabPage from '@/mixins/tabPage'
33
-import { joinApiUrl } from '@/config'
33
+import { resolveResourceUrl } from '@/utils/resourceUrl'
34 34
 
35 35
 const POSTER = '/static/ai/hero.png'
36 36
 
@@ -65,10 +65,10 @@ export default {
65 65
     this.displayDate = this.decodeQuery(q, 'date') || this.$t('courseDetailPage.noDate')
66 66
 
67 67
     const src = this.decodeQuery(q, 'src')
68
-    this.videoSrc = src ? joinApiUrl(src) : ''
68
+    this.videoSrc = src ? resolveResourceUrl(src) : ''
69 69
 
70 70
     const cover = this.decodeQuery(q, 'cover')
71
-    this.posterSrc = cover ? joinApiUrl(cover) : POSTER
71
+    this.posterSrc = cover ? resolveResourceUrl(cover) : POSTER
72 72
   },
73 73
   onShow() {
74 74
     const p = uni.setNavigationBarTitle({

+ 2 - 2
ruoyi-ui-app/package-a/livestock-resources/index.vue

@@ -84,7 +84,7 @@ import UTabs from 'uview-plus/components/u-tabs/u-tabs.vue'
84 84
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
85 85
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
86 86
 import tabPage from '@/mixins/tabPage'
87
-import { joinApiUrl } from '@/config'
87
+import { resolveResourceUrl } from '@/utils/resourceUrl'
88 88
 import { putNewsDetailPayload } from '@/utils/newsDetailCache'
89 89
 import { listLivestockResource } from '@/api/livestockResource'
90 90
 
@@ -351,7 +351,7 @@ export default {
351 351
     },
352 352
     articleCover(item) {
353 353
       if (item.coverFileUrl) {
354
-        return joinApiUrl(item.coverFileUrl)
354
+        return resolveResourceUrl(item.coverFileUrl)
355 355
       }
356 356
       return this.coverSrc
357 357
     },

+ 2 - 2
ruoyi-ui-app/package-a/my-enrollment/index.vue

@@ -97,7 +97,7 @@ import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
97 97
 import ULazyLoad from 'uview-plus/components/u-lazy-load/u-lazy-load.vue'
98 98
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
99 99
 import tabPage from '@/mixins/tabPage'
100
-import { joinApiUrl } from '@/config'
100
+import { resolveResourceUrl } from '@/utils/resourceUrl'
101 101
 import { listMyEnroll } from '@/api/practicalTraining'
102 102
 
103 103
 const TRAINING_STATUS_CODES = ['006001', '006002', '006003', '006004']
@@ -362,7 +362,7 @@ export default {
362 362
     },
363 363
     cardCover(card) {
364 364
       if (card.coverFileUrl) {
365
-        return joinApiUrl(card.coverFileUrl)
365
+        return resolveResourceUrl(card.coverFileUrl)
366 366
       }
367 367
       return this.coverSrc
368 368
     },

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

@@ -93,7 +93,7 @@
93 93
 <script>
94 94
 import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
95 95
 import tabPage from '@/mixins/tabPage'
96
-import { joinApiUrl } from '@/config'
96
+import { resolveResourceUrl } from '@/utils/resourceUrl'
97 97
 import { fileNameFromPath, openOnlinePreview, resolveMediaKind } from '@/utils/filePreview'
98 98
 import { takeNewsDetailPayload } from '@/utils/newsDetailCache'
99 99
 
@@ -154,10 +154,10 @@ export default {
154 154
       return !!(this.contentFileUrl || '').trim()
155 155
     },
156 156
     coverFullUrl() {
157
-      return joinApiUrl(this.coverFileUrl)
157
+      return resolveResourceUrl(this.coverFileUrl)
158 158
     },
159 159
     contentFullUrl() {
160
-      return joinApiUrl(this.contentFileUrl)
160
+      return resolveResourceUrl(this.contentFileUrl)
161 161
     },
162 162
     attachmentKind() {
163 163
       return resolveMediaKind(this.contentFileUrl)

+ 2 - 2
ruoyi-ui-app/package-a/online-clinic/index.vue

@@ -95,7 +95,7 @@ import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
95 95
 import UButton from 'uview-plus/components/u-button/u-button.vue'
96 96
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
97 97
 import tabPage from '@/mixins/tabPage'
98
-import { joinApiUrl } from '@/config'
98
+import { resolveResourceUrl } from '@/utils/resourceUrl'
99 99
 import { ensureApiToken } from '@/utils/apiAuth'
100 100
 import {
101 101
   listOnlineConsultVets,
@@ -330,7 +330,7 @@ export default {
330 330
     },
331 331
     avatarUrl(item) {
332 332
       if (item && item.photoFileUrl) {
333
-        return joinApiUrl(item.photoFileUrl)
333
+        return resolveResourceUrl(item.photoFileUrl)
334 334
       }
335 335
       return ''
336 336
     },

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

@@ -45,7 +45,7 @@
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 { joinApiUrl } from '@/config'
48
+import { resolveResourceUrl } from '@/utils/resourceUrl'
49 49
 import {
50 50
   getMyEnrollDetail,
51 51
   enrollPracticalTraining,
@@ -213,7 +213,7 @@ export default {
213 213
     this.total = Number.isFinite(tot) && tot >= 0 ? tot : 0
214 214
 
215 215
     const cover = this.decodeQuery(q, 'cover')
216
-    this.coverSrc = cover ? joinApiUrl(cover) : COVER
216
+    this.coverSrc = cover ? resolveResourceUrl(cover) : COVER
217 217
 
218 218
     this.applyQueryDates(q)
219 219
   },
@@ -273,7 +273,7 @@ export default {
273 273
         this.total = Number(d.plannedHeadCount)
274 274
       }
275 275
       if (d.coverFileUrl) {
276
-        this.coverSrc = joinApiUrl(d.coverFileUrl)
276
+        this.coverSrc = resolveResourceUrl(d.coverFileUrl)
277 277
       }
278 278
       uni.setNavigationBarTitle({
279 279
         title: this.displayTitleRaw || this.$t(this.navTitleKey)

+ 2 - 2
ruoyi-ui-app/package-a/vet-profile/index.vue

@@ -49,7 +49,7 @@
49 49
 import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
50 50
 import UButton from 'uview-plus/components/u-button/u-button.vue'
51 51
 import tabPage from '@/mixins/tabPage'
52
-import { joinApiUrl } from '@/config'
52
+import { resolveResourceUrl } from '@/utils/resourceUrl'
53 53
 import { ensureApiToken } from '@/utils/apiAuth'
54 54
 import {
55 55
   loadVetProfileCache,
@@ -80,7 +80,7 @@ export default {
80 80
     },
81 81
     photoSrc() {
82 82
       const url = this.vet && this.vet.photoFileUrl
83
-      return url ? joinApiUrl(url) : ''
83
+      return url ? resolveResourceUrl(url) : ''
84 84
     },
85 85
     vetTitle() {
86 86
       return (this.vet && this.vet.resourceName) || this.$t('vetProfilePage.nameFallback')

+ 2 - 2
ruoyi-ui-app/pages/message/index.vue

@@ -82,7 +82,7 @@ import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
82 82
 import UAvatar from 'uview-plus/components/u-avatar/u-avatar.vue'
83 83
 import UVirtualList from 'uview-plus/components/u-virtual-list/u-virtual-list.vue'
84 84
 import tabPage from '@/mixins/tabPage'
85
-import { joinApiUrl } from '@/config'
85
+import { resolveResourceUrl } from '@/utils/resourceUrl'
86 86
 import { ensureApiToken } from '@/utils/apiAuth'
87 87
 import {
88 88
   listAskerConsultSessions,
@@ -316,7 +316,7 @@ export default {
316 316
       const fromProfile = loadVetProfileCache(id)
317 317
       const fromBooking = loadBookingResourceCache('004001', id)
318 318
       const url = (fromProfile && fromProfile.photoFileUrl) || (fromBooking && fromBooking.photoFileUrl)
319
-      return url ? joinApiUrl(url) : ''
319
+      return url ? resolveResourceUrl(url) : ''
320 320
     },
321 321
     avatarBg(row) {
322 322
       const colors = ['#B8C5B8', '#C9B8A8', '#A8B4C9', '#C4B8C8', '#B5A896']

+ 2 - 12
ruoyi-ui-app/store/user.js

@@ -1,7 +1,7 @@
1 1
 import { reactive } from 'vue'
2 2
 import { login as loginApi, logout as logoutApi, getInfo } from '@/api/login'
3 3
 import { getToken, setToken, removeToken } from '@/utils/auth'
4
-import { joinApiUrl } from '@/config'
4
+import { resolveResourceUrl } from '@/utils/resourceUrl'
5 5
 
6 6
 const state = reactive({
7 7
   token: getToken(),
@@ -13,18 +13,8 @@ const state = reactive({
13 13
   permissions: []
14 14
 })
15 15
 
16
-function isHttp(url) {
17
-  return /^https?:\/\//i.test(url || '')
18
-}
19
-
20 16
 function resolveAvatar(path) {
21
-  if (!path) {
22
-    return ''
23
-  }
24
-  if (isHttp(path)) {
25
-    return path
26
-  }
27
-  return joinApiUrl(path)
17
+  return resolveResourceUrl(path)
28 18
 }
29 19
 
30 20
 export function useUserStore() {

+ 2 - 4
ruoyi-ui-app/utils/aiConsult.js

@@ -1,4 +1,4 @@
1
-import { joinApiUrl } from '@/config'
1
+import { resolveResourceUrl } from '@/utils/resourceUrl'
2 2
 
3 3
 /** 与后端 ConsultSessionRules 一致 */
4 4
 export const SENDER_ROLE_USER = 1
@@ -142,9 +142,7 @@ export function isVetMessage(m) {
142 142
 }
143 143
 
144 144
 export function mediaUrl(url) {
145
-  if (!url) return ''
146
-  if (/^https?:\/\//i.test(url)) return url
147
-  return joinApiUrl(url)
145
+  return resolveResourceUrl(url)
148 146
 }
149 147
 
150 148
 export function normalizeMessage(m) {

+ 2 - 2
ruoyi-ui-app/utils/filePreview.js

@@ -1,4 +1,4 @@
1
-import { joinApiUrl } from '@/config'
1
+import { resolveResourceUrl } from '@/utils/resourceUrl'
2 2
 
3 3
 const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp)$/i
4 4
 const VIDEO_RE = /\.(mp4|mov|m4v|webm|avi)$/i
@@ -29,7 +29,7 @@ export function fileNameFromPath(path) {
29 29
 
30 30
 /** 在线预览完整 URL(/profile/** 后端 permitAll,可直接访问) */
31 31
 export function getOnlinePreviewUrl(path) {
32
-  return joinApiUrl(path)
32
+  return resolveResourceUrl(path)
33 33
 }
34 34
 
35 35
 const FILE_PREVIEW_PAGE = '/package-a/file-preview/index'

+ 38 - 0
ruoyi-ui-app/utils/resourceUrl.js

@@ -0,0 +1,38 @@
1
+import { joinApiUrl } from '@/config'
2
+import { isExternal } from '@/utils/validate'
3
+
4
+/**
5
+ * 将接口返回的资源路径转为可访问 URL(对齐 ruoyi-ui ImagePreview)
6
+ * - 外链 / data: / blob: 原样返回
7
+ * - 相对路径拼接 BASE_API(H5 生产为当前浏览器 origin + /prod-api)
8
+ * @param {string} src 支持逗号分隔时取第一项
9
+ */
10
+export function resolveResourceUrl(src) {
11
+  if (!src) {
12
+    return ''
13
+  }
14
+  const path = String(src).split(',')[0].trim()
15
+  if (!path) {
16
+    return ''
17
+  }
18
+  if (isExternal(path)) {
19
+    return path
20
+  }
21
+  return joinApiUrl(path)
22
+}
23
+
24
+/**
25
+ * 多图预览列表(对齐 ImagePreview realSrcList)
26
+ * @param {string} src 逗号分隔
27
+ * @returns {string[]}
28
+ */
29
+export function resolveResourceUrlList(src) {
30
+  if (!src) {
31
+    return []
32
+  }
33
+  return String(src)
34
+    .split(',')
35
+    .map((item) => item.trim())
36
+    .filter(Boolean)
37
+    .map((item) => (isExternal(item) ? item : joinApiUrl(item)))
38
+}

+ 12 - 0
ruoyi-ui-app/utils/validate.js

@@ -0,0 +1,12 @@
1
+/**
2
+ * 判断 path 是否为外链(对齐 ruoyi-ui/src/utils/validate.js)
3
+ * @param {string} path
4
+ */
5
+export function isExternal(path) {
6
+  return /^(https?:|mailto:|tel:|data:|blob:)/.test(path || '')
7
+}
8
+
9
+/** 是否为 http(s) 绝对地址 */
10
+export function isHttp(url) {
11
+  return /^https?:\/\//i.test(url || '')
12
+}