xsh_1997 преди 1 седмица
родител
ревизия
47422cb824

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

@@ -10,7 +10,7 @@
10
 			</view>
10
 			</view>
11
 		</view>
11
 		</view>
12
 		<view class="cart-item__main" @click="emit('goods-click', item)">
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
 			<view class="cart-item__info">
14
 			<view class="cart-item__info">
15
 				<text class="cart-item__name">{{ item.goodsName }}</text>
15
 				<text class="cart-item__name">{{ item.goodsName }}</text>
16
 				<view v-if="item.specList && item.specList.length" class="cart-item__specs">
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
 <template>
1
 <template>
2
 	<view class="review-card">
2
 	<view class="review-card">
3
 		<view class="review-card__head">
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
 			<view class="review-card__meta">
5
 			<view class="review-card__meta">
6
 				<text class="review-card__name">{{ item.memberNickName }}</text>
6
 				<text class="review-card__name">{{ item.memberNickName }}</text>
7
 				<view v-if="item.score" class="review-card__stars">
7
 				<view v-if="item.score" class="review-card__stars">
@@ -12,13 +12,14 @@
12
 		</view>
12
 		</view>
13
 		<text class="review-card__content">{{ item.content }}</text>
13
 		<text class="review-card__content">{{ item.content }}</text>
14
 		<view v-if="item.pics && item.pics.length" class="review-card__pics">
14
 		<view v-if="item.pics && item.pics.length" class="review-card__pics">
15
-			<image
15
+			<image-preview
16
 				v-for="(pic, idx) in item.pics"
16
 				v-for="(pic, idx) in item.pics"
17
 				:key="idx"
17
 				:key="idx"
18
 				class="review-card__pic"
18
 				class="review-card__pic"
19
 				:src="pic"
19
 				:src="pic"
20
-				mode="aspectFill"
21
-				@click="previewPics(idx)"
20
+				preview
21
+				:preview-list="item.pics"
22
+				:preview-index="idx"
22
 			/>
23
 			/>
23
 		</view>
24
 		</view>
24
 		<view v-if="item.replyContent" class="review-card__reply">
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
 </script>
43
 </script>
49
 
44
 
50
 <style lang="scss" scoped>
45
 <style lang="scss" scoped>

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

@@ -6,7 +6,7 @@
6
 			class="goods-card"
6
 			class="goods-card"
7
 			@click="emit('item-click', item)"
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
 			<view class="goods-card__body">
10
 			<view class="goods-card__body">
11
 				<text class="goods-card__name">{{ item.goodsName }}</text>
11
 				<text class="goods-card__name">{{ item.goodsName }}</text>
12
 				<view class="goods-card__price">
12
 				<view class="goods-card__price">

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

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

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

@@ -1,6 +1,6 @@
1
 <template>
1
 <template>
2
 	<view class="checkout-goods">
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
 		<view class="checkout-goods__info">
4
 		<view class="checkout-goods__info">
5
 			<text class="checkout-goods__name">{{ goods.goodsName }}</text>
5
 			<text class="checkout-goods__name">{{ goods.goodsName }}</text>
6
 			<view v-if="goods.specList && goods.specList.length" class="checkout-goods__specs">
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
 <template>
1
 <template>
2
 	<view class="multi-goods-row">
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
 		<view class="multi-goods-row__main">
4
 		<view class="multi-goods-row__main">
5
 			<text class="multi-goods-row__name">{{ item.goodsName }}</text>
5
 			<text class="multi-goods-row__name">{{ item.goodsName }}</text>
6
 			<view v-if="item.specList && item.specList.length" class="multi-goods-row__specs">
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
 		<text v-if="label" class="img-grid__label">{{ label }}</text>
3
 		<text v-if="label" class="img-grid__label">{{ label }}</text>
4
 		<view class="img-grid__list">
4
 		<view class="img-grid__list">
5
 			<view v-for="(url, index) in modelValue" :key="index" class="img-grid__item">
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
 				<view class="img-grid__del" @click="removeAt(index)">×</view>
7
 				<view class="img-grid__del" @click="removeAt(index)">×</view>
8
 			</view>
8
 			</view>
9
 			<view
9
 			<view
@@ -20,7 +20,6 @@
20
 
20
 
21
 <script setup>
21
 <script setup>
22
 import { ref } from 'vue'
22
 import { ref } from 'vue'
23
-import { joinApiUrl } from '@/config'
24
 import { uploadFile } from '@/utils/upload'
23
 import { uploadFile } from '@/utils/upload'
25
 
24
 
26
 const props = defineProps({
25
 const props = defineProps({
@@ -42,12 +41,6 @@ const emit = defineEmits(['update:modelValue'])
42
 
41
 
43
 const uploading = ref(false)
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
 function removeAt(index) {
44
 function removeAt(index) {
52
 	const next = [...props.modelValue]
45
 	const next = [...props.modelValue]
53
 	next.splice(index, 1)
46
 	next.splice(index, 1)

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

@@ -1,6 +1,6 @@
1
 <template>
1
 <template>
2
 	<view class="order-goods-row" @click="emit('click')">
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
 		<view class="order-goods-row__main" :class="{ 'order-goods-row__main--compact': reviewAction }">
4
 		<view class="order-goods-row__main" :class="{ 'order-goods-row__main--compact': reviewAction }">
5
 			<text class="order-goods-row__name">{{ item.goodsName }}</text>
5
 			<text class="order-goods-row__name">{{ item.goodsName }}</text>
6
 			<view v-if="item.specList && item.specList.length" class="order-goods-row__specs">
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
 <template>
1
 <template>
2
 	<view class="review-done-card">
2
 	<view class="review-done-card">
3
 		<view class="review-done-card__goods">
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
 			<view class="review-done-card__goods-meta">
5
 			<view class="review-done-card__goods-meta">
6
 				<text class="review-done-card__name">{{ item.goodsName }}</text>
6
 				<text class="review-done-card__name">{{ item.goodsName }}</text>
7
 				<text v-if="item.specText" class="review-done-card__spec">{{ item.specText }}</text>
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
 			class="shop-card"
6
 			class="shop-card"
7
 			@click="emit('item-click', item)"
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
 			<view class="shop-card__body">
10
 			<view class="shop-card__body">
11
 				<text class="shop-card__name">{{ item.shopName }}</text>
11
 				<text class="shop-card__name">{{ item.shopName }}</text>
12
 				<view v-if="item.showRating || item.showFans" class="shop-card__meta">
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
 			class="follow-card"
6
 			class="follow-card"
7
 			@click="emit('item-click', item)"
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
 			<view class="follow-card__body">
10
 			<view class="follow-card__body">
11
 				<view class="follow-card__title-row">
11
 				<view class="follow-card__title-row">
12
 					<text class="follow-card__name">{{ item.shopName }}</text>
12
 					<text class="follow-card__name">{{ item.shopName }}</text>

+ 2 - 1
shop-app/pages.json

@@ -4,7 +4,8 @@
4
 		"custom": {
4
 		"custom": {
5
 			"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
5
 			"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
6
 			"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
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
 	"pages": [
11
 	"pages": [

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

@@ -28,7 +28,7 @@
28
 						class="right-item"
28
 						class="right-item"
29
 						@click="onLevel2Tap(child)"
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
 						<text v-if="child.isHot" class="right-item__badge">热</text>
32
 						<text v-if="child.isHot" class="right-item__badge">热</text>
33
 						<text class="right-item__name">{{ child.categoryName }}</text>
33
 						<text class="right-item__name">{{ child.categoryName }}</text>
34
 					</view>
34
 					</view>

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

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

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

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

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

@@ -17,7 +17,13 @@
17
 					:circular="detail.pics.length > 1"
17
 					:circular="detail.pics.length > 1"
18
 				>
18
 				>
19
 					<swiper-item v-for="(pic, idx) in detail.pics" :key="idx">
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
 					</swiper-item>
27
 					</swiper-item>
22
 				</swiper>
28
 				</swiper>
23
 
29
 
@@ -87,7 +93,7 @@
87
 				<view v-if="detail.services.length" class="card section-card">
93
 				<view v-if="detail.services.length" class="card section-card">
88
 					<view class="section-title">服务说明</view>
94
 					<view class="section-title">服务说明</view>
89
 					<view v-for="svc in detail.services" :key="svc.serviceId" class="service-item">
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
 						<view class="service-body">
97
 						<view class="service-body">
92
 							<text class="service-name">{{ svc.serviceName }}</text>
98
 							<text class="service-name">{{ svc.serviceName }}</text>
93
 							<text v-if="svc.serviceIntro" class="service-intro">{{ svc.serviceIntro }}</text>
99
 							<text v-if="svc.serviceIntro" class="service-intro">{{ svc.serviceIntro }}</text>
@@ -127,7 +133,7 @@
127
 
133
 
128
 				<!-- 店铺入口 -->
134
 				<!-- 店铺入口 -->
129
 				<view class="card shop-card" @click="onEnterShop">
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
 					<view class="shop-info">
137
 					<view class="shop-info">
132
 						<text class="shop-name">{{ detail.shop.shopName || '—' }}</text>
138
 						<text class="shop-name">{{ detail.shop.shopName || '—' }}</text>
133
 						<text class="shop-enter">进店逛逛 ></text>
139
 						<text class="shop-enter">进店逛逛 ></text>
@@ -150,7 +156,7 @@
150
 		<u-popup :show="qtyPopupShow" mode="bottom" round="16" @close="qtyPopupShow = false">
156
 		<u-popup :show="qtyPopupShow" mode="bottom" round="16" @close="qtyPopupShow = false">
151
 			<view class="qty-popup">
157
 			<view class="qty-popup">
152
 				<view class="qty-popup__head">
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
 					<view>
160
 					<view>
155
 						<text class="qty-popup__price">¥ {{ detail?.priceText }}</text>
161
 						<text class="qty-popup__price">¥ {{ detail?.priceText }}</text>
156
 						<text class="qty-popup__stock">库存 {{ detail?.stock }}</text>
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
 function openQtyPopup() {
295
 function openQtyPopup() {
295
 	if (!detail.value) return
296
 	if (!detail.value) return
296
 	qtyPopupShow.value = true
297
 	qtyPopupShow.value = true

+ 5 - 12
shop-app/subpackage/order/aftersale-detail.vue

@@ -18,7 +18,7 @@
18
 			<view class="as-card">
18
 			<view class="as-card">
19
 				<text class="as-card__title">商品信息</text>
19
 				<text class="as-card__title">商品信息</text>
20
 				<view class="as-goods">
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
 					<view class="as-goods__info">
22
 					<view class="as-goods__info">
23
 						<text class="as-goods__name">{{ detail.goodsName }}</text>
23
 						<text class="as-goods__name">{{ detail.goodsName }}</text>
24
 						<text
24
 						<text
@@ -73,13 +73,14 @@
73
 			<view v-if="detail.evidencePics && detail.evidencePics.length" class="as-card">
73
 			<view v-if="detail.evidencePics && detail.evidencePics.length" class="as-card">
74
 				<text class="as-card__title">凭证图片</text>
74
 				<text class="as-card__title">凭证图片</text>
75
 				<view class="as-pics">
75
 				<view class="as-pics">
76
-					<image
76
+					<image-preview
77
 						v-for="(pic, idx) in detail.evidencePics"
77
 						v-for="(pic, idx) in detail.evidencePics"
78
 						:key="idx"
78
 						:key="idx"
79
 						class="as-pics__item"
79
 						class="as-pics__item"
80
 						:src="pic"
80
 						:src="pic"
81
-						mode="aspectFill"
82
-						@click="previewPic(idx)"
81
+						preview
82
+						:preview-list="detail.evidencePics"
83
+						:preview-index="idx"
83
 					/>
84
 					/>
84
 				</view>
85
 				</view>
85
 			</view>
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
 onLoad((options) => {
130
 onLoad((options) => {
138
 	aftersaleId.value = options.aftersaleId || ''
131
 	aftersaleId.value = options.aftersaleId || ''
139
 	if (!aftersaleId.value) {
132
 	if (!aftersaleId.value) {

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

@@ -39,7 +39,7 @@
39
 						<text class="aftersale-card__status">{{ row.statusText }}</text>
39
 						<text class="aftersale-card__status">{{ row.statusText }}</text>
40
 					</view>
40
 					</view>
41
 					<view class="aftersale-card__goods">
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
 						<view class="aftersale-card__info">
43
 						<view class="aftersale-card__info">
44
 							<text class="aftersale-card__name">{{ row.goodsName }}</text>
44
 							<text class="aftersale-card__name">{{ row.goodsName }}</text>
45
 							<text class="aftersale-card__type">{{ row.applyTypeText }} · {{ row.applyReason }}</text>
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
 				<address-bar :address="preview.address" @click="onAddressTap" />
15
 				<address-bar :address="preview.address" @click="onAddressTap" />
16
 
16
 
17
 				<view class="shop-head">
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
 					<text class="shop-head__name">{{ preview.shop.shopName }}</text>
19
 					<text class="shop-head__name">{{ preview.shop.shopName }}</text>
20
 				</view>
20
 				</view>
21
 
21
 

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

@@ -15,7 +15,7 @@
15
 				<address-bar :address="preview.address" @click="onAddressTap" />
15
 				<address-bar :address="preview.address" @click="onAddressTap" />
16
 
16
 
17
 				<view class="shop-head">
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
 					<text class="shop-head__name">{{ preview.shop.shopName }}</text>
19
 					<text class="shop-head__name">{{ preview.shop.shopName }}</text>
20
 				</view>
20
 				</view>
21
 
21
 

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

@@ -34,7 +34,7 @@
34
 
34
 
35
 				<view class="detail-card">
35
 				<view class="detail-card">
36
 					<view class="detail-shop">
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
 						<text class="detail-shop__name">{{ detail.shopName }}</text>
38
 						<text class="detail-shop__name">{{ detail.shopName }}</text>
39
 					</view>
39
 					</view>
40
 					<order-goods-row
40
 					<order-goods-row

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

@@ -49,7 +49,7 @@
49
 					@click="onCardClick(card)"
49
 					@click="onCardClick(card)"
50
 				>
50
 				>
51
 					<view class="order-card__head">
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
 						<text class="order-card__shop-name">{{ card.shopName }}</text>
53
 						<text class="order-card__shop-name">{{ card.shopName }}</text>
54
 						<text
54
 						<text
55
 							class="order-card__status"
55
 							class="order-card__status"

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

@@ -26,13 +26,14 @@
26
 				<text class="review-view-card__label">评价内容</text>
26
 				<text class="review-view-card__label">评价内容</text>
27
 				<text class="review-view-content">{{ review.content || '(无文字评价)' }}</text>
27
 				<text class="review-view-content">{{ review.content || '(无文字评价)' }}</text>
28
 				<view v-if="review.pics && review.pics.length" class="review-view-pics">
28
 				<view v-if="review.pics && review.pics.length" class="review-view-pics">
29
-					<image
29
+					<image-preview
30
 						v-for="(pic, idx) in review.pics"
30
 						v-for="(pic, idx) in review.pics"
31
 						:key="idx"
31
 						:key="idx"
32
 						class="review-view-pic"
32
 						class="review-view-pic"
33
 						:src="pic"
33
 						:src="pic"
34
-						mode="aspectFill"
35
-						@click="previewPics(idx)"
34
+						preview
35
+						:preview-list="review.pics"
36
+						:preview-index="idx"
36
 					/>
37
 					/>
37
 				</view>
38
 				</view>
38
 				<text v-if="review.createTime" class="review-view-time">评价时间:{{ review.createTime }}</text>
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
 onLoad((options) => {
104
 onLoad((options) => {
109
 	if (!ensureApiToken(true)) return
105
 	if (!ensureApiToken(true)) return
110
 	orderId.value = options.orderId || ''
106
 	orderId.value = options.orderId || ''

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

@@ -13,7 +13,7 @@
13
 
13
 
14
 			<template v-else-if="profile">
14
 			<template v-else-if="profile">
15
 				<view class="shop-header">
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
 					<view class="shop-header__main">
17
 					<view class="shop-header__main">
18
 						<view class="shop-header__title-row">
18
 						<view class="shop-header__title-row">
19
 							<text class="shop-header__name">{{ profile.shopName }}</text>
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
   return joinApiUrl(raw.startsWith('/') ? raw : `/${raw}`)
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
+}