xsh_1997 vor 1 Woche
Ursprung
Commit
b31871d76a

+ 1 - 1
shop-app/components/cart/CartItemRow.vue

@@ -10,7 +10,7 @@
10 10
 			</view>
11 11
 		</view>
12 12
 		<view class="cart-item__main" @click="emit('goods-click', item)">
13
-			<image-preview class="cart-item__pic" :src="item.mainPic || item.displayPic" />
13
+			<image-preview class="cart-item__pic" :src="item.mainPic" />
14 14
 			<view class="cart-item__info">
15 15
 				<text class="cart-item__name">{{ item.goodsName }}</text>
16 16
 				<view v-if="item.specList && item.specList.length" class="cart-item__specs">

+ 2 - 2
shop-app/components/common/ImagePreview.vue

@@ -42,10 +42,10 @@ const props = defineProps({
42 42
 		type: String,
43 43
 		default: 'aspectFill'
44 44
 	},
45
-	/** 加载失败或 src 为空时的占位图 */
45
+	/** 无图或加载失败时的占位图;默认空则显示灰色图标(避免误显示 logo) */
46 46
 	placeholder: {
47 47
 		type: String,
48
-		default: '/static/logo.png'
48
+		default: ''
49 49
 	},
50 50
 	/** 点击预览大图 */
51 51
 	preview: {

+ 1 - 1
shop-app/components/mall/GoodsGrid.vue

@@ -6,7 +6,7 @@
6 6
 			class="goods-card"
7 7
 			@click="emit('item-click', item)"
8 8
 		>
9
-			<image-preview class="goods-card__pic" :src="item.mainPic || item.displayPic" />
9
+			<image-preview class="goods-card__pic" :src="item.mainPic" />
10 10
 			<view class="goods-card__body">
11 11
 				<text class="goods-card__name">{{ item.goodsName }}</text>
12 12
 				<view class="goods-card__price">

+ 1 - 1
shop-app/components/order/CheckoutGoodsRow.vue

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
 	<view class="checkout-goods">
3
-		<image-preview class="checkout-goods__pic" :src="goods.mainPic || goods.displayPic" />
3
+		<image-preview class="checkout-goods__pic" :src="goods.mainPic" />
4 4
 		<view class="checkout-goods__info">
5 5
 			<text class="checkout-goods__name">{{ goods.goodsName }}</text>
6 6
 			<view v-if="goods.specList && goods.specList.length" class="checkout-goods__specs">

+ 1 - 1
shop-app/components/order/CheckoutMultiGoodsRow.vue

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
 	<view class="multi-goods-row">
3
-		<image-preview class="multi-goods-row__pic" :src="item.mainPic || item.displayPic" />
3
+		<image-preview class="multi-goods-row__pic" :src="item.mainPic" />
4 4
 		<view class="multi-goods-row__main">
5 5
 			<text class="multi-goods-row__name">{{ item.goodsName }}</text>
6 6
 			<view v-if="item.specList && item.specList.length" class="multi-goods-row__specs">

+ 1 - 1
shop-app/components/order/OrderGoodsRow.vue

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
 	<view class="order-goods-row" @click="emit('click')">
3
-		<image-preview class="order-goods-row__pic" :src="item.goodsImage || item.mainPic || item.displayPic" />
3
+		<image-preview class="order-goods-row__pic" :src="item.goodsImage || item.mainPic" />
4 4
 		<view class="order-goods-row__main" :class="{ 'order-goods-row__main--compact': reviewAction }">
5 5
 			<text class="order-goods-row__name">{{ item.goodsName }}</text>
6 6
 			<view v-if="item.specList && item.specList.length" class="order-goods-row__specs">

+ 1 - 1
shop-app/components/order/ReviewDoneSummaryCard.vue

@@ -1,7 +1,7 @@
1 1
 <template>
2 2
 	<view class="review-done-card">
3 3
 		<view class="review-done-card__goods">
4
-			<image-preview class="review-done-card__pic" :src="item.goodsImage || item.mainPic || item.displayPic" />
4
+			<image-preview class="review-done-card__pic" :src="item.goodsImage || item.mainPic" />
5 5
 			<view class="review-done-card__goods-meta">
6 6
 				<text class="review-done-card__name">{{ item.goodsName }}</text>
7 7
 				<text v-if="item.specText" class="review-done-card__spec">{{ item.specText }}</text>

+ 25 - 0
shop-app/config/index.js

@@ -34,6 +34,31 @@ function resolveBaseApi() {
34 34
 
35 35
 export const BASE_API = resolveBaseApi()
36 36
 
37
+/** 后端完整地址(含 IP/域名),来自 .env 的 VITE_APP_API_HOST */
38
+export const API_HOST = apiHost
39
+
40
+/**
41
+ * 图片/文件访问根地址(始终用完整 IP/域名)
42
+ * 说明:<image> 标签不会走 Vite 的 /dev-api 代理,必须用 API_HOST 直连后端
43
+ */
44
+export const FILE_BASE = API_HOST
45
+
46
+/**
47
+ * 拼接图片、上传文件等静态资源地址(profile/upload 等)
48
+ * @param {string} path
49
+ */
50
+export function joinFileUrl(path) {
51
+  if (!path) {
52
+    return FILE_BASE
53
+  }
54
+  if (/^https?:\/\//i.test(path)) {
55
+    return path
56
+  }
57
+  const p = path.startsWith('/') ? path : '/' + path
58
+  const base = String(FILE_BASE).replace(/\/$/, '')
59
+  return base + p
60
+}
61
+
37 62
 /**
38 63
  * 拼接接口或静态资源地址(/login、/profile/...)
39 64
  * @param {string} path

+ 1 - 1
shop-app/pages/category/index.vue

@@ -28,7 +28,7 @@
28 28
 						class="right-item"
29 29
 						@click="onLevel2Tap(child)"
30 30
 					>
31
-						<image-preview class="right-item__pic" :src="child.categoryPic || child.displayPic" />
31
+						<image-preview class="right-item__pic" :src="child.categoryPic" />
32 32
 						<text v-if="child.isHot" class="right-item__badge">热</text>
33 33
 						<text class="right-item__name">{{ child.categoryName }}</text>
34 34
 					</view>

+ 10 - 12
shop-app/pages/index/index.vue

@@ -36,7 +36,7 @@
36 36
 					<swiper-item v-for="(item, index) in bannerList" :key="item.bannerId || index">
37 37
 						<image-preview
38 38
 							class="banner-img"
39
-							:src="item.bannerImage"
39
+							:src="item.bannerImageRaw || item.bannerImage"
40 40
 							preview
41 41
 							:preview-list="bannerPreviewList"
42 42
 							:preview-index="index"
@@ -57,7 +57,7 @@
57 57
 						>
58 58
 							<image-preview
59 59
 								class="category-icon"
60
-								:src="item.categoryPic || item.displayPic"
60
+								:src="item.categoryPic"
61 61
 							/>
62 62
 							<text class="category-name">{{ item.categoryName }}</text>
63 63
 						</view>
@@ -95,7 +95,7 @@
95 95
 						class="hot-card"
96 96
 						@click="onGoodsTap(item)"
97 97
 					>
98
-						<image-preview class="hot-pic" :src="item.mainPic || item.displayPic" />
98
+						<image-preview class="hot-pic" :src="item.mainPic" />
99 99
 						<view class="hot-info">
100 100
 							<text class="hot-name">{{ item.goodsName }}</text>
101 101
 							<view class="hot-meta">
@@ -183,15 +183,13 @@ async function fetchModule(apiFn, mapper) {
183 183
 /** Banner:过滤无效图(BN13) */
184 184
 function mapBanners(list) {
185 185
 	return list
186
-		.map((row) => {
187
-			const url = resolveFileUrl(row.bannerImage)
188
-			return {
189
-				bannerId: row.bannerId,
190
-				bannerImage: url,
191
-				sortNo: row.sortNo
192
-			}
193
-		})
194
-		.filter((item) => !!item.bannerImage)
186
+		.map((row) => ({
187
+			bannerId: row.bannerId,
188
+			bannerImageRaw: row.bannerImage || '',
189
+			bannerImage: resolveFileUrl(row.bannerImage),
190
+			sortNo: row.sortNo
191
+		}))
192
+		.filter((item) => !!item.bannerImageRaw)
195 193
 }
196 194
 
197 195
 function mapCategories(list) {

+ 1 - 1
shop-app/subpackage/category/level1.vue

@@ -39,7 +39,7 @@
39 39
 							@click.stop="onTabChange(index)"
40 40
 						>
41 41
 							<view class="l2-item__pic-wrap">
42
-								<image-preview class="l2-item__pic" :src="tab.categoryPic || tab.displayPic" :placeholder="CATEGORY_PLACEHOLDER" />
42
+								<image-preview class="l2-item__pic" :src="tab.categoryPic" :placeholder="CATEGORY_PLACEHOLDER" />
43 43
 								<text v-if="tab.isHot" class="l2-item__hot">热</text>
44 44
 							</view>
45 45
 							<text class="l2-item__name">{{ tab.categoryName }}</text>

+ 1 - 1
shop-app/subpackage/order/aftersale-detail.vue

@@ -18,7 +18,7 @@
18 18
 			<view class="as-card">
19 19
 				<text class="as-card__title">商品信息</text>
20 20
 				<view class="as-goods">
21
-					<image-preview class="as-goods__pic" :src="detail.goodsImage || detail.displayPic" />
21
+					<image-preview class="as-goods__pic" :src="detail.goodsImage" />
22 22
 					<view class="as-goods__info">
23 23
 						<text class="as-goods__name">{{ detail.goodsName }}</text>
24 24
 						<text

+ 1 - 1
shop-app/subpackage/order/aftersale-list.vue

@@ -39,7 +39,7 @@
39 39
 						<text class="aftersale-card__status">{{ row.statusText }}</text>
40 40
 					</view>
41 41
 					<view class="aftersale-card__goods">
42
-						<image-preview class="aftersale-card__pic" :src="row.goodsImage || row.displayPic" />
42
+						<image-preview class="aftersale-card__pic" :src="row.goodsImage" />
43 43
 						<view class="aftersale-card__info">
44 44
 							<text class="aftersale-card__name">{{ row.goodsName }}</text>
45 45
 							<text class="aftersale-card__type">{{ row.applyTypeText }} · {{ row.applyReason }}</text>

+ 1 - 0
shop-app/utils/cartDisplay.js

@@ -36,6 +36,7 @@ function mapCartItem(row) {
36 36
     cartItemId: row.cartItemId,
37 37
     goodsId: row.goodsId,
38 38
     goodsName: row.goodsName || '',
39
+    mainPic: row.mainPic || '',
39 40
     displayPic: resolveFileUrl(row.mainPic) || GOODS_PLACEHOLDER,
40 41
     specText: (row.specText || '').trim() || '默认',
41 42
     specList: parseCartSpecText(row.specText),

+ 2 - 1
shop-app/utils/checkoutDisplay.js

@@ -18,6 +18,7 @@ export function mapGoodsItem(item) {
18 18
     cartItemId: item.cartItemId,
19 19
     goodsId: item.goodsId,
20 20
     goodsName: item.goodsName || '',
21
+    mainPic: item.mainPic || '',
21 22
     displayPic: resolveFileUrl(item.mainPic) || GOODS_PLACEHOLDER,
22 23
     specText,
23 24
     specList: parseCartSpecText(specText),
@@ -41,7 +42,7 @@ function mapShopAndAddress(data) {
41 42
     shop: {
42 43
       shopId: shop.shopId,
43 44
       shopName: shop.shopName || '',
44
-      shopAvatar: resolveFileUrl(shop.shopAvatar) || SHOP_PLACEHOLDER,
45
+      shopAvatar: shop.shopAvatar || '',
45 46
       shopStatus: shop.shopStatus
46 47
     },
47 48
     address: addr

+ 42 - 6
shop-app/utils/image.js

@@ -1,4 +1,6 @@
1
-import { joinApiUrl } from '@/config'
1
+import { joinFileUrl, API_HOST } from '@/config'
2
+
3
+const PLACEHOLDER_PATHS = ['/static/logo.png']
2 4
 
3 5
 /** 是否外链或 data URL */
4 6
 export function isExternalUrl(url) {
@@ -6,26 +8,60 @@ export function isExternalUrl(url) {
6 8
   return /^(https?:|data:|\/\/)/i.test(String(url).trim())
7 9
 }
8 10
 
11
+/** 去掉 /dev-api 等代理前缀,得到后端真实路径 */
12
+function stripApiProxyPrefix(path) {
13
+  return String(path).replace(/^\/(dev-api|prod-api|shop-api)/, '') || '/'
14
+}
15
+
16
+function isPlaceholderPath(path) {
17
+  const raw = String(path || '').trim()
18
+  return PLACEHOLDER_PATHS.includes(raw)
19
+}
20
+
9 21
 /**
10
- * 将后端返回的图片路径转为可展示的完整 URL
22
+ * 是否已是可直接使用的图片地址(避免重复拼接)
23
+ */
24
+export function isResolvedFileUrl(url) {
25
+  if (!url) return false
26
+  const raw = String(url).trim()
27
+  if (isExternalUrl(raw)) return true
28
+  if (raw.startsWith('/static/')) return true
29
+  const apiHost = String(API_HOST).replace(/\/$/, '')
30
+  return !!(apiHost && (raw === apiHost || raw.startsWith(`${apiHost}/`)))
31
+}
32
+
33
+/**
34
+ * 将后端返回的图片路径转为可展示的完整 URL(可重复调用)
35
+ * 结果形如:http://192.168.1.6:8020/profile/upload/xxx.jpg
11 36
  * @param {string} path bannerImage / categoryPic / mainPic 等
12 37
  */
13 38
 export function resolveFileUrl(path) {
14 39
   if (path == null || path === '') {
15 40
     return ''
16 41
   }
17
-  const raw = String(path).split(',')[0].trim()
18
-  if (!raw) {
42
+  let raw = String(path).split(',')[0].trim()
43
+  if (!raw || isPlaceholderPath(raw)) {
19 44
     return ''
20 45
   }
21 46
   if (isExternalUrl(raw)) {
22 47
     return raw
23 48
   }
24
-  return joinApiUrl(raw.startsWith('/') ? raw : `/${raw}`)
49
+  if (raw.startsWith('/static/')) {
50
+    return raw
51
+  }
52
+  const apiHost = String(API_HOST).replace(/\/$/, '')
53
+  if (apiHost && (raw === apiHost || raw.startsWith(`${apiHost}/`))) {
54
+    return raw
55
+  }
56
+  if (/^\/(dev-api|prod-api|shop-api)\//i.test(raw)) {
57
+    raw = stripApiProxyPrefix(raw)
58
+  }
59
+  const normalized = raw.startsWith('/') ? raw : `/${raw}`
60
+  return joinFileUrl(normalized)
25 61
 }
26 62
 
27 63
 /**
28
- * 将逗号分隔或数组形式的图片路径转为完整 URL 列表(对齐 ruoyi-ui ImagePreview)
64
+ * 将逗号分隔或数组形式的图片路径转为完整 URL 列表
29 65
  * @param {string|string[]|null|undefined} pathOrList
30 66
  */
31 67
 export function resolveFileUrlList(pathOrList) {

+ 7 - 0
shop-app/utils/orderDisplay.js

@@ -52,6 +52,8 @@ function mapFirstItem(item) {
52 52
     orderItemId: item.itemId || item.orderItemId,
53 53
     goodsId: item.goodsId,
54 54
     goodsName: item.goodsName || '',
55
+    goodsImage: item.goodsImage || item.mainPic || '',
56
+    mainPic: item.mainPic || '',
55 57
     displayPic: resolveFileUrl(item.goodsImage || item.mainPic) || GOODS_PLACEHOLDER,
56 58
     specText,
57 59
     specList: parseCartSpecText(specText),
@@ -70,6 +72,8 @@ function mapOrderItemRow(row) {
70 72
     orderItemId: row.orderItemId || row.itemId,
71 73
     goodsId: row.goodsId,
72 74
     goodsName: row.goodsName || '',
75
+    goodsImage: row.goodsImage || row.mainPic || '',
76
+    mainPic: row.mainPic || '',
73 77
     displayPic: resolveFileUrl(row.goodsImage || row.mainPic) || GOODS_PLACEHOLDER,
74 78
     specText,
75 79
     specList: parseCartSpecText(specText),
@@ -341,6 +345,8 @@ export function mapAftersaleListRow(row) {
341 345
     applyAmountText: formatPrice(row.applyAmount),
342 346
     createTime: row.createTime || '',
343 347
     goodsName: row.goodsName || '',
348
+    goodsImage: row.goodsImage || row.mainPic || '',
349
+    mainPic: row.mainPic || '',
344 350
     displayPic: resolveFileUrl(row.goodsImage || row.mainPic) || GOODS_PLACEHOLDER,
345 351
     specList: parseCartSpecText(specText),
346 352
     orderId: row.orderId,
@@ -383,6 +389,7 @@ export function mapAftersaleDetail(data) {
383 389
     orderNo: info.orderNo || data.orderNo || '',
384 390
     orderItemId: info.orderItemId,
385 391
     goodsName: info.goodsName || '',
392
+    goodsImage: info.goodsImage || info.mainPic || '',
386 393
     displayPic: resolveFileUrl(info.goodsImage || info.mainPic) || GOODS_PLACEHOLDER,
387 394
     specList: parseCartSpecText(specText),
388 395
     quantity: Number(info.quantity) || 1