Browse Source

图片添加地址

xsh_1997 1 week ago
parent
commit
47422cb824

+ 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 class="cart-item__pic" :src="item.displayPic" mode="aspectFill" />
13
+			<image-preview class="cart-item__pic" :src="item.mainPic || item.displayPic" />
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">

+ 156 - 0
shop-app/components/common/ImagePreview.vue

@@ -0,0 +1,156 @@
1
+<template>
2
+	<view class="image-preview" :style="wrapStyle" @click="onTap">
3
+		<image
4
+			v-if="showImage"
5
+			class="image-preview__img"
6
+			:src="realSrc"
7
+			:mode="mode"
8
+			@error="onError"
9
+			@load="onLoad"
10
+		/>
11
+		<view v-else class="image-preview__slot">
12
+			<image
13
+				v-if="placeholder"
14
+				class="image-preview__img"
15
+				:src="placeholder"
16
+				:mode="mode"
17
+			/>
18
+			<u-icon v-else name="photo" color="#c0c4cc" :size="iconSize" />
19
+		</view>
20
+	</view>
21
+</template>
22
+
23
+<script setup>
24
+import { ref, computed, watch } from 'vue'
25
+import { resolveFileUrl, resolveFileUrlList } from '@/utils/image'
26
+
27
+const props = defineProps({
28
+	/** 后端路径或逗号分隔多图(展示首张) */
29
+	src: {
30
+		type: String,
31
+		default: ''
32
+	},
33
+	width: {
34
+		type: [String, Number],
35
+		default: ''
36
+	},
37
+	height: {
38
+		type: [String, Number],
39
+		default: ''
40
+	},
41
+	mode: {
42
+		type: String,
43
+		default: 'aspectFill'
44
+	},
45
+	/** 加载失败或 src 为空时的占位图 */
46
+	placeholder: {
47
+		type: String,
48
+		default: '/static/logo.png'
49
+	},
50
+	/** 点击预览大图 */
51
+	preview: {
52
+		type: Boolean,
53
+		default: false
54
+	},
55
+	/** 预览列表;不传则从 src 逗号拆分 */
56
+	previewList: {
57
+		type: Array,
58
+		default: () => []
59
+	},
60
+	previewIndex: {
61
+		type: Number,
62
+		default: 0
63
+	},
64
+	iconSize: {
65
+		type: [String, Number],
66
+		default: 28
67
+	}
68
+})
69
+
70
+const loadFailed = ref(false)
71
+
72
+const realSrc = computed(() => {
73
+	if (!props.src) return ''
74
+	return resolveFileUrl(props.src)
75
+})
76
+
77
+const resolvedPreviewList = computed(() => {
78
+	if (props.previewList && props.previewList.length) {
79
+		return resolveFileUrlList(props.previewList)
80
+	}
81
+	return resolveFileUrlList(props.src)
82
+})
83
+
84
+const showImage = computed(() => !!(realSrc.value && !loadFailed.value))
85
+
86
+const wrapStyle = computed(() => {
87
+	const style = {}
88
+	if (props.width !== '' && props.width != null) {
89
+		style.width = formatSize(props.width)
90
+	}
91
+	if (props.height !== '' && props.height != null) {
92
+		style.height = formatSize(props.height)
93
+	}
94
+	return style
95
+})
96
+
97
+watch(
98
+	() => props.src,
99
+	() => {
100
+		loadFailed.value = false
101
+	}
102
+)
103
+
104
+function formatSize(val) {
105
+	if (typeof val === 'number') {
106
+		return `${val}rpx`
107
+	}
108
+	const s = String(val)
109
+	if (/^\d+$/.test(s)) {
110
+		return `${s}rpx`
111
+	}
112
+	return s
113
+}
114
+
115
+function onError() {
116
+	loadFailed.value = true
117
+}
118
+
119
+function onLoad() {
120
+	loadFailed.value = false
121
+}
122
+
123
+function onTap() {
124
+	if (!props.preview) return
125
+	const urls = resolvedPreviewList.value
126
+	if (!urls.length) return
127
+	const idx = Math.min(Math.max(props.previewIndex, 0), urls.length - 1)
128
+	uni.previewImage({
129
+		urls,
130
+		current: urls[idx]
131
+	})
132
+}
133
+</script>
134
+
135
+<style lang="scss" scoped>
136
+.image-preview {
137
+	display: block;
138
+	width: 100%;
139
+	height: 100%;
140
+	overflow: hidden;
141
+	background: #ebeef5;
142
+}
143
+.image-preview__img {
144
+	width: 100%;
145
+	height: 100%;
146
+	display: block;
147
+}
148
+.image-preview__slot {
149
+	width: 100%;
150
+	height: 100%;
151
+	display: flex;
152
+	align-items: center;
153
+	justify-content: center;
154
+	background: #ebeef5;
155
+}
156
+</style>

+ 6 - 11
shop-app/components/goods/ReviewCard.vue

@@ -1,7 +1,7 @@
1 1
 <template>
2 2
 	<view class="review-card">
3 3
 		<view class="review-card__head">
4
-			<image class="review-card__avatar" :src="avatarSrc" mode="aspectFill" />
4
+			<image-preview class="review-card__avatar" :src="item.memberAvatar || avatarSrc" />
5 5
 			<view class="review-card__meta">
6 6
 				<text class="review-card__name">{{ item.memberNickName }}</text>
7 7
 				<view v-if="item.score" class="review-card__stars">
@@ -12,13 +12,14 @@
12 12
 		</view>
13 13
 		<text class="review-card__content">{{ item.content }}</text>
14 14
 		<view v-if="item.pics && item.pics.length" class="review-card__pics">
15
-			<image
15
+			<image-preview
16 16
 				v-for="(pic, idx) in item.pics"
17 17
 				:key="idx"
18 18
 				class="review-card__pic"
19 19
 				:src="pic"
20
-				mode="aspectFill"
21
-				@click="previewPics(idx)"
20
+				preview
21
+				:preview-list="item.pics"
22
+				:preview-index="idx"
22 23
 			/>
23 24
 		</view>
24 25
 		<view v-if="item.replyContent" class="review-card__reply">
@@ -38,13 +39,7 @@ const props = defineProps({
38 39
 	}
39 40
 })
40 41
 
41
-const avatarSrc = computed(() => props.item.memberAvatar || '/static/logo.png')
42
-
43
-function previewPics(index) {
44
-	const urls = props.item.pics || []
45
-	if (!urls.length) return
46
-	uni.previewImage({ urls, current: urls[index] })
47
-}
42
+const avatarSrc = computed(() => '/static/logo.png')
48 43
 </script>
49 44
 
50 45
 <style lang="scss" scoped>

+ 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 class="goods-card__pic" :src="item.displayPic" mode="aspectFill" />
9
+			<image-preview class="goods-card__pic" :src="item.mainPic || item.displayPic" />
10 10
 			<view class="goods-card__body">
11 11
 				<text class="goods-card__name">{{ item.goodsName }}</text>
12 12
 				<view class="goods-card__price">

+ 5 - 13
shop-app/components/mine/ImageUpload.vue

@@ -2,11 +2,11 @@
2 2
 	<view class="img-upload">
3 3
 		<text v-if="label" class="img-upload__label">{{ label }}</text>
4 4
 		<view class="img-upload__box" @click="chooseImage">
5
-			<image
6
-				v-if="displayUrl"
5
+			<image-preview
6
+				v-if="modelValue"
7 7
 				class="img-upload__preview"
8
-				:src="displayUrl"
9
-				mode="aspectFill"
8
+				:src="modelValue"
9
+				preview
10 10
 			/>
11 11
 			<view v-else class="img-upload__placeholder">
12 12
 				<u-icon name="camera-fill" color="#9a938c" size="40" />
@@ -20,8 +20,7 @@
20 20
 </template>
21 21
 
22 22
 <script setup>
23
-import { ref, computed } from 'vue'
24
-import { joinApiUrl } from '@/config'
23
+import { ref } from 'vue'
25 24
 import { uploadFile } from '@/utils/upload'
26 25
 
27 26
 const props = defineProps({
@@ -34,13 +33,6 @@ const emit = defineEmits(['update:modelValue'])
34 33
 
35 34
 const uploading = ref(false)
36 35
 
37
-const displayUrl = computed(() => {
38
-  const v = props.modelValue || ''
39
-  if (!v) return ''
40
-  if (/^https?:\/\//i.test(v)) return v
41
-  return joinApiUrl(v)
42
-})
43
-
44 36
 function chooseImage() {
45 37
   if (uploading.value) return
46 38
   uni.chooseImage({

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

@@ -1,6 +1,6 @@
1 1
 <template>
2 2
 	<view class="checkout-goods">
3
-		<image class="checkout-goods__pic" :src="goods.displayPic" mode="aspectFill" />
3
+		<image-preview class="checkout-goods__pic" :src="goods.mainPic || goods.displayPic" />
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 class="multi-goods-row__pic" :src="item.displayPic" mode="aspectFill" />
3
+		<image-preview class="multi-goods-row__pic" :src="item.mainPic || item.displayPic" />
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 - 8
shop-app/components/order/ImageUploadGrid.vue

@@ -3,7 +3,7 @@
3 3
 		<text v-if="label" class="img-grid__label">{{ label }}</text>
4 4
 		<view class="img-grid__list">
5 5
 			<view v-for="(url, index) in modelValue" :key="index" class="img-grid__item">
6
-				<image class="img-grid__pic" :src="displayUrl(url)" mode="aspectFill" />
6
+				<image-preview class="img-grid__pic" :src="url" preview />
7 7
 				<view class="img-grid__del" @click="removeAt(index)">×</view>
8 8
 			</view>
9 9
 			<view
@@ -20,7 +20,6 @@
20 20
 
21 21
 <script setup>
22 22
 import { ref } from 'vue'
23
-import { joinApiUrl } from '@/config'
24 23
 import { uploadFile } from '@/utils/upload'
25 24
 
26 25
 const props = defineProps({
@@ -42,12 +41,6 @@ const emit = defineEmits(['update:modelValue'])
42 41
 
43 42
 const uploading = ref(false)
44 43
 
45
-function displayUrl(url) {
46
-	if (!url) return ''
47
-	if (/^https?:\/\//i.test(url)) return url
48
-	return joinApiUrl(url)
49
-}
50
-
51 44
 function removeAt(index) {
52 45
 	const next = [...props.modelValue]
53 46
 	next.splice(index, 1)

+ 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 class="order-goods-row__pic" :src="item.displayPic" mode="aspectFill" />
3
+		<image-preview class="order-goods-row__pic" :src="item.goodsImage || item.mainPic || item.displayPic" />
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 class="review-done-card__pic" :src="item.displayPic" mode="aspectFill" />
4
+			<image-preview class="review-done-card__pic" :src="item.goodsImage || item.mainPic || item.displayPic" />
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>

+ 1 - 1
shop-app/components/search/ShopList.vue

@@ -6,7 +6,7 @@
6 6
 			class="shop-card"
7 7
 			@click="emit('item-click', item)"
8 8
 		>
9
-			<image class="shop-card__avatar" :src="item.displayAvatar" mode="aspectFill" />
9
+			<image-preview class="shop-card__avatar" :src="item.shopAvatar || item.displayAvatar" />
10 10
 			<view class="shop-card__body">
11 11
 				<text class="shop-card__name">{{ item.shopName }}</text>
12 12
 				<view v-if="item.showRating || item.showFans" class="shop-card__meta">

+ 1 - 1
shop-app/components/shop/ShopFollowList.vue

@@ -6,7 +6,7 @@
6 6
 			class="follow-card"
7 7
 			@click="emit('item-click', item)"
8 8
 		>
9
-			<image class="follow-card__avatar" :src="item.displayAvatar" mode="aspectFill" />
9
+			<image-preview class="follow-card__avatar" :src="item.shopAvatar || item.displayAvatar" />
10 10
 			<view class="follow-card__body">
11 11
 				<view class="follow-card__title-row">
12 12
 					<text class="follow-card__name">{{ item.shopName }}</text>

+ 2 - 1
shop-app/pages.json

@@ -4,7 +4,8 @@
4 4
 		"custom": {
5 5
 			"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
6 6
 			"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
7
-			"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue"
7
+			"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
8
+			"^image-preview$": "@/components/common/ImagePreview.vue"
8 9
 		}
9 10
 	},
10 11
 	"pages": [

+ 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 class="right-item__pic" :src="child.displayPic" mode="aspectFill" />
31
+						<image-preview class="right-item__pic" :src="child.categoryPic || child.displayPic" />
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>

+ 11 - 18
shop-app/pages/index/index.vue

@@ -34,11 +34,12 @@
34 34
 					duration="500"
35 35
 				>
36 36
 					<swiper-item v-for="(item, index) in bannerList" :key="item.bannerId || index">
37
-						<image
37
+						<image-preview
38 38
 							class="banner-img"
39 39
 							:src="item.bannerImage"
40
-							mode="aspectFill"
41
-							@click="onBannerPreview(index)"
40
+							preview
41
+							:preview-list="bannerPreviewList"
42
+							:preview-index="index"
42 43
 						/>
43 44
 					</swiper-item>
44 45
 				</swiper>
@@ -54,11 +55,9 @@
54 55
 							class="category-item"
55 56
 							@click="onCategoryTap(item)"
56 57
 						>
57
-							<image
58
+							<image-preview
58 59
 								class="category-icon"
59
-								:src="item.displayPic"
60
-								mode="aspectFill"
61
-								@error="onCategoryImgError(item)"
60
+								:src="item.categoryPic || item.displayPic"
62 61
 							/>
63 62
 							<text class="category-name">{{ item.categoryName }}</text>
64 63
 						</view>
@@ -96,7 +95,7 @@
96 95
 						class="hot-card"
97 96
 						@click="onGoodsTap(item)"
98 97
 					>
99
-						<image class="hot-pic" :src="item.displayPic" mode="aspectFill" />
98
+						<image-preview class="hot-pic" :src="item.mainPic || item.displayPic" />
100 99
 						<view class="hot-info">
101 100
 							<text class="hot-name">{{ item.goodsName }}</text>
102 101
 							<view class="hot-meta">
@@ -146,6 +145,10 @@ const categoryLoadFailed = ref(false)
146 145
 
147 146
 const headerTotalHeight = computed(() => statusBarHeight.value + headerContentHeight)
148 147
 
148
+const bannerPreviewList = computed(() =>
149
+	bannerList.value.map((b) => b.bannerImage).filter(Boolean)
150
+)
151
+
149 152
 /** 并行拉取三接口,各模块独立渲染(HM14) */
150 153
 function loadHomeData() {
151 154
 	hotLoading.value = true
@@ -237,12 +240,6 @@ function onSearchTap() {
237 240
 	uni.navigateTo({ url: PAGE_SEARCH_INDEX })
238 241
 }
239 242
 
240
-function onBannerPreview(index) {
241
-	const urls = bannerList.value.map((b) => b.bannerImage).filter(Boolean)
242
-	if (!urls.length) return
243
-	uni.previewImage({ urls, current: urls[index] || urls[0] })
244
-}
245
-
246 243
 function onCategoryTap(item) {
247 244
 	uni.navigateTo({
248 245
 		url:
@@ -256,10 +253,6 @@ function onMoreCategoryTap() {
256 253
 	uni.switchTab({ url: PAGE_CATEGORY_TAB })
257 254
 }
258 255
 
259
-function onCategoryImgError(item) {
260
-	item.displayPic = CATEGORY_PLACEHOLDER
261
-}
262
-
263 256
 function onGoodsTap(item) {
264 257
 	goGoodsDetail(item.goodsId)
265 258
 }

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

@@ -2,7 +2,7 @@
2 2
 	<view class="mine-page">
3 3
 		<view class="mine-header" @click="onHeaderClick">
4 4
 			<view class="mine-header__row">
5
-				<image class="mine-header__avatar" :src="avatarUrl" mode="aspectFill" />
5
+				<image-preview class="mine-header__avatar" :src="avatarUrl" />
6 6
 				<view class="mine-header__info">
7 7
 					<text class="mine-header__name">{{ headerTitle }}</text>
8 8
 					<text v-if="loggedIn && mobileText" class="mine-header__sub">{{ mobileText }}</text>

+ 2 - 7
shop-app/subpackage/category/level1.vue

@@ -5,7 +5,7 @@
5 5
 		<view v-if="level1Name" class="cat-panel">
6 6
 			<!-- 手风琴标题:一级分类,点击展开/收起二级 -->
7 7
 			<view class="l1-hero" @click="toggleAccordion">
8
-				<image class="l1-hero__pic" :src="level1DisplayPic" mode="aspectFill" />
8
+				<image-preview class="l1-hero__pic" :src="level1Pic" :placeholder="CATEGORY_PLACEHOLDER" />
9 9
 				<view class="l1-hero__info">
10 10
 					<view class="l1-hero__title-row">
11 11
 						<text class="l1-hero__name">{{ level1Name }}</text>
@@ -39,7 +39,7 @@
39 39
 							@click.stop="onTabChange(index)"
40 40
 						>
41 41
 							<view class="l2-item__pic-wrap">
42
-								<image class="l2-item__pic" :src="tab.displayPic" mode="aspectFill" />
42
+								<image-preview class="l2-item__pic" :src="tab.categoryPic || tab.displayPic" :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>
@@ -66,7 +66,6 @@ import { ref, computed } from 'vue'
66 66
 import { onLoad, onShow } from '@dcloudio/uni-app'
67 67
 import { getCategoryTree, getLevel2Tabs } from '@/api/category'
68 68
 import { mapCategoryTree, mapLevel2Tabs } from '@/utils/categoryDisplay'
69
-import { resolveFileUrl } from '@/utils/image'
70 69
 import { DEFAULT_SORT_BY } from '@/constants/categorySort'
71 70
 import SearchEntry from '@/components/mall/SearchEntry.vue'
72 71
 import GoodsListBlock from '@/components/category/GoodsListBlock.vue'
@@ -93,10 +92,6 @@ const activeCategoryId = computed(() => {
93 92
 	return tab ? tab.categoryId : ''
94 93
 })
95 94
 
96
-const level1DisplayPic = computed(
97
-	() => resolveFileUrl(level1Pic.value) || CATEGORY_PLACEHOLDER
98
-)
99
-
100 95
 const activeTabName = computed(() => {
101 96
 	const tab = tabList.value[activeTabIndex.value]
102 97
 	return tab ? tab.categoryName : ''

+ 10 - 9
shop-app/subpackage/goods/detail.vue

@@ -17,7 +17,13 @@
17 17
 					:circular="detail.pics.length > 1"
18 18
 				>
19 19
 					<swiper-item v-for="(pic, idx) in detail.pics" :key="idx">
20
-						<image class="pic-swiper__img" :src="pic" mode="aspectFill" @click="previewPics(idx)" />
20
+						<image-preview
21
+							class="pic-swiper__img"
22
+							:src="pic"
23
+							preview
24
+							:preview-list="detail.pics"
25
+							:preview-index="idx"
26
+						/>
21 27
 					</swiper-item>
22 28
 				</swiper>
23 29
 
@@ -87,7 +93,7 @@
87 93
 				<view v-if="detail.services.length" class="card section-card">
88 94
 					<view class="section-title">服务说明</view>
89 95
 					<view v-for="svc in detail.services" :key="svc.serviceId" class="service-item">
90
-						<image v-if="svc.serviceIcon" class="service-icon" :src="svc.serviceIcon" mode="aspectFit" />
96
+						<image-preview v-if="svc.serviceIcon" class="service-icon" :src="svc.serviceIcon" mode="aspectFit" />
91 97
 						<view class="service-body">
92 98
 							<text class="service-name">{{ svc.serviceName }}</text>
93 99
 							<text v-if="svc.serviceIntro" class="service-intro">{{ svc.serviceIntro }}</text>
@@ -127,7 +133,7 @@
127 133
 
128 134
 				<!-- 店铺入口 -->
129 135
 				<view class="card shop-card" @click="onEnterShop">
130
-					<image class="shop-avatar" :src="detail.shop.shopAvatar" mode="aspectFill" />
136
+					<image-preview class="shop-avatar" :src="detail.shop.shopAvatar" />
131 137
 					<view class="shop-info">
132 138
 						<text class="shop-name">{{ detail.shop.shopName || '—' }}</text>
133 139
 						<text class="shop-enter">进店逛逛 ></text>
@@ -150,7 +156,7 @@
150 156
 		<u-popup :show="qtyPopupShow" mode="bottom" round="16" @close="qtyPopupShow = false">
151 157
 			<view class="qty-popup">
152 158
 				<view class="qty-popup__head">
153
-					<image class="qty-popup__pic" :src="detail?.pics?.[0]" mode="aspectFill" />
159
+					<image-preview class="qty-popup__pic" :src="detail?.pics?.[0]" />
154 160
 					<view>
155 161
 						<text class="qty-popup__price">¥ {{ detail?.priceText }}</text>
156 162
 						<text class="qty-popup__stock">库存 {{ detail?.stock }}</text>
@@ -286,11 +292,6 @@ async function loadReviewPreview() {
286 292
 	}
287 293
 }
288 294
 
289
-function previewPics(index) {
290
-	const urls = detail.value?.pics || []
291
-	uni.previewImage({ urls, current: urls[index] })
292
-}
293
-
294 295
 function openQtyPopup() {
295 296
 	if (!detail.value) return
296 297
 	qtyPopupShow.value = true

+ 5 - 12
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 class="as-goods__pic" :src="detail.displayPic" mode="aspectFill" />
21
+					<image-preview class="as-goods__pic" :src="detail.goodsImage || detail.displayPic" />
22 22
 					<view class="as-goods__info">
23 23
 						<text class="as-goods__name">{{ detail.goodsName }}</text>
24 24
 						<text
@@ -73,13 +73,14 @@
73 73
 			<view v-if="detail.evidencePics && detail.evidencePics.length" class="as-card">
74 74
 				<text class="as-card__title">凭证图片</text>
75 75
 				<view class="as-pics">
76
-					<image
76
+					<image-preview
77 77
 						v-for="(pic, idx) in detail.evidencePics"
78 78
 						:key="idx"
79 79
 						class="as-pics__item"
80 80
 						:src="pic"
81
-						mode="aspectFill"
82
-						@click="previewPic(idx)"
81
+						preview
82
+						:preview-list="detail.evidencePics"
83
+						:preview-index="idx"
83 84
 					/>
84 85
 				</view>
85 86
 			</view>
@@ -126,14 +127,6 @@ async function loadDetail() {
126 127
 	}
127 128
 }
128 129
 
129
-function previewPic(index) {
130
-	if (!detail.value || !detail.value.evidencePics) return
131
-	uni.previewImage({
132
-		current: index,
133
-		urls: detail.value.evidencePics
134
-	})
135
-}
136
-
137 130
 onLoad((options) => {
138 131
 	aftersaleId.value = options.aftersaleId || ''
139 132
 	if (!aftersaleId.value) {

+ 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 class="aftersale-card__pic" :src="row.displayPic" mode="aspectFill" />
42
+						<image-preview class="aftersale-card__pic" :src="row.goodsImage || row.displayPic" />
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 - 1
shop-app/subpackage/order/checkout-cart.vue

@@ -15,7 +15,7 @@
15 15
 				<address-bar :address="preview.address" @click="onAddressTap" />
16 16
 
17 17
 				<view class="shop-head">
18
-					<image class="shop-head__avatar" :src="preview.shop.shopAvatar" mode="aspectFill" />
18
+					<image-preview class="shop-head__avatar" :src="preview.shop.shopAvatar" />
19 19
 					<text class="shop-head__name">{{ preview.shop.shopName }}</text>
20 20
 				</view>
21 21
 

+ 1 - 1
shop-app/subpackage/order/checkout.vue

@@ -15,7 +15,7 @@
15 15
 				<address-bar :address="preview.address" @click="onAddressTap" />
16 16
 
17 17
 				<view class="shop-head">
18
-					<image class="shop-head__avatar" :src="preview.shop.shopAvatar" mode="aspectFill" />
18
+					<image-preview class="shop-head__avatar" :src="preview.shop.shopAvatar" />
19 19
 					<text class="shop-head__name">{{ preview.shop.shopName }}</text>
20 20
 				</view>
21 21
 

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

@@ -34,7 +34,7 @@
34 34
 
35 35
 				<view class="detail-card">
36 36
 					<view class="detail-shop">
37
-						<image class="detail-shop__avatar" :src="detail.shopAvatar" mode="aspectFill" />
37
+						<image-preview class="detail-shop__avatar" :src="detail.shopAvatar" />
38 38
 						<text class="detail-shop__name">{{ detail.shopName }}</text>
39 39
 					</view>
40 40
 					<order-goods-row

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

@@ -49,7 +49,7 @@
49 49
 					@click="onCardClick(card)"
50 50
 				>
51 51
 					<view class="order-card__head">
52
-						<image class="order-card__shop-avatar" :src="card.shopAvatar" mode="aspectFill" />
52
+						<image-preview class="order-card__shop-avatar" :src="card.shopAvatar" />
53 53
 						<text class="order-card__shop-name">{{ card.shopName }}</text>
54 54
 						<text
55 55
 							class="order-card__status"

+ 4 - 8
shop-app/subpackage/order/review-view.vue

@@ -26,13 +26,14 @@
26 26
 				<text class="review-view-card__label">评价内容</text>
27 27
 				<text class="review-view-content">{{ review.content || '(无文字评价)' }}</text>
28 28
 				<view v-if="review.pics && review.pics.length" class="review-view-pics">
29
-					<image
29
+					<image-preview
30 30
 						v-for="(pic, idx) in review.pics"
31 31
 						:key="idx"
32 32
 						class="review-view-pic"
33 33
 						:src="pic"
34
-						mode="aspectFill"
35
-						@click="previewPics(idx)"
34
+						preview
35
+						:preview-list="review.pics"
36
+						:preview-index="idx"
36 37
 					/>
37 38
 				</view>
38 39
 				<text v-if="review.createTime" class="review-view-time">评价时间:{{ review.createTime }}</text>
@@ -100,11 +101,6 @@ async function loadReview() {
100 101
 	}
101 102
 }
102 103
 
103
-function previewPics(index) {
104
-	const urls = review.value?.pics || []
105
-	uni.previewImage({ urls, current: urls[index] })
106
-}
107
-
108 104
 onLoad((options) => {
109 105
 	if (!ensureApiToken(true)) return
110 106
 	orderId.value = options.orderId || ''

+ 1 - 1
shop-app/subpackage/shop/index.vue

@@ -13,7 +13,7 @@
13 13
 
14 14
 			<template v-else-if="profile">
15 15
 				<view class="shop-header">
16
-					<image class="shop-header__avatar" :src="profile.shopAvatar" mode="aspectFill" />
16
+					<image-preview class="shop-header__avatar" :src="profile.shopAvatar" />
17 17
 					<view class="shop-header__main">
18 18
 						<view class="shop-header__title-row">
19 19
 							<text class="shop-header__name">{{ profile.shopName }}</text>

+ 15 - 0
shop-app/utils/image.js

@@ -23,3 +23,18 @@ export function resolveFileUrl(path) {
23 23
   }
24 24
   return joinApiUrl(raw.startsWith('/') ? raw : `/${raw}`)
25 25
 }
26
+
27
+/**
28
+ * 将逗号分隔或数组形式的图片路径转为完整 URL 列表(对齐 ruoyi-ui ImagePreview)
29
+ * @param {string|string[]|null|undefined} pathOrList
30
+ */
31
+export function resolveFileUrlList(pathOrList) {
32
+  if (pathOrList == null || pathOrList === '') return []
33
+  if (Array.isArray(pathOrList)) {
34
+    return pathOrList.map((item) => resolveFileUrl(item)).filter(Boolean)
35
+  }
36
+  return String(pathOrList)
37
+    .split(',')
38
+    .map((s) => resolveFileUrl(s.trim()))
39
+    .filter(Boolean)
40
+}