Просмотр исходного кода

feat(search & collect): enhance product selection and search attribution

- Updated the addCollectAjax function to accept additional parameters for sourceFrom and searchWord, improving the ability to track search origins.
- Modified the select-good-popup-host and related components to handle searchWord, allowing for better attribution in product selection.
- Enhanced navigation to detail pages by including searchWord in the URL for improved tracking of user interactions.
- Adjusted various components to ensure consistent handling of sourceFrom and searchWord, enhancing overall user experience during product interactions.
ylong 2 недель назад
Родитель
Сommit
a2a1ed82ec

+ 2 - 2
api/modules/mall.js

@@ -137,8 +137,8 @@ export const useMallApi = (Vue, vm) => {
         // 获取收藏列表
         getCollectListAjax: (params) => vm.$u.get('/token/shop/user/collectList', params),
 
-        // 添加收藏
-        addCollectAjax: (isbn) => vm.$u.post('/token/shop/user/addCollect', { isbn }),
+        // 添加收藏,可带 sourceFrom、searchWord 做搜索归因
+        addCollectAjax: (data) => vm.$u.post('/token/shop/user/addCollect', typeof data === 'string' ? { isbn: data } : data),
 
         // 取消收藏
         removeCollectAjax: (isbnList) => vm.$u.post('/token/shop/user/removeCollect', { isbnList }),

+ 2 - 2
components/select-good-popup-host.vue

@@ -13,9 +13,9 @@ export default {
     },
     mounted() {
         this.openHandler = (payload = {}) => {
-            const { product, sourceFrom } = payload
+            const { product, sourceFrom, searchWord } = payload
             if (!product) return
-            this.$refs.popup?.open(product, sourceFrom)
+            this.$refs.popup?.open(product, sourceFrom, searchWord)
         }
         eventBus.on('openSelectGoodPopup', this.openHandler)
     },

+ 13 - 4
pages-sell/components/hot-recommend-item/index.vue

@@ -43,13 +43,20 @@ export default {
 			type: Object,
 			required: true,
 			default: () => ({})
+		},
+		// 当前这次搜索实际使用的词,用于搜索效果归因
+		searchWord: {
+			type: String,
+			default: ''
 		}
 	},
 	methods: {
 		navigateToDetail() {
-			uni.navigateTo({
-				url: '/pages-sell/pages/detail?isbn=' + this.item.isbn
-			});
+			let url = '/pages-sell/pages/detail?isbn=' + this.item.isbn + '&sourceFrom=1';
+			if (this.searchWord) {
+				url += '&searchWord=' + encodeURIComponent(this.searchWord);
+			}
+			uni.navigateTo({ url });
 		},
 		handleAction() {
 			if (this.item.availableStock === 0) {
@@ -77,7 +84,9 @@ export default {
 			});
 		},
 		handleAddToCart() {
-			eventBus.emit('openSelectGoodPopup', { product: this.item, sourceFrom: 1 });
+			const payload = { product: this.item, sourceFrom: 1 };
+			if (this.searchWord) payload.searchWord = this.searchWord;
+			eventBus.emit('openSelectGoodPopup', payload);
 		},
 	}
 }

+ 1 - 1
pages-sell/components/hot-sell-item/index.vue

@@ -90,7 +90,7 @@ export default {
 				...this.item,
 				isbn: this.item.bookIsbn || this.item.isbn || '',
 			};
-			eventBus.emit('openSelectGoodPopup', { product, sourceFrom: 1 });
+			eventBus.emit('openSelectGoodPopup', { product });
 		},
 	}
 }

+ 23 - 7
pages-sell/components/recommend-item/index.vue

@@ -59,6 +59,15 @@
 			showDesc: {
 				type: Boolean,
 				default: true
+			},
+			// 1-搜索页 2-书单 3-商品详情,由使用方显式传入
+			sourceFrom: {
+				type: Number,
+				default: 0
+			},
+			searchWord: {
+				type: String,
+				default: ''
 			}
 		},
 		computed: {
@@ -103,16 +112,23 @@
 				});
 			},
 			handleAddToCart() {
-				eventBus.emit('openSelectGoodPopup', {
+				const payload = {
 					product: this.item,
-					sourceFrom: this.showDesc ? 2 : 1,
-				});
+					sourceFrom: this.sourceFrom,
+				};
+				if (this.searchWord) payload.searchWord = this.searchWord;
+				eventBus.emit('openSelectGoodPopup', payload);
 			},
-			//图书详情页
+			// 仅搜索来源把来源和搜索词带到详情,详情页加购/收藏才能记成搜索页
 			handleClick() {
-				uni.navigateTo({
-					url: '/pages-sell/pages/detail?isbn=' + this.item.isbn
-				});
+				let url = '/pages-sell/pages/detail?isbn=' + this.item.isbn;
+				if (Number(this.sourceFrom) === 1) {
+					url += '&sourceFrom=1';
+					if (this.searchWord) {
+						url += '&searchWord=' + encodeURIComponent(this.searchWord);
+					}
+				}
+				uni.navigateTo({ url });
 			}
 		}
 	}

+ 15 - 13
pages-sell/components/select-good-popup/index.vue

@@ -106,6 +106,7 @@ export default {
 			currentProduct: {},
 			qualityOptions: [],
 			sourceFrom: 0,
+			searchWord: '',
 			isSubmitting: false, // 防重复提交标志
 		};
 	},
@@ -140,14 +141,25 @@ export default {
 		}
 	},
 	methods: {
-		open(product, sourceFrom) {
+		open(product, sourceFrom, searchWord) {
 			this.visible = true;
 			this.quantity = 1;
 			this.sourceFrom = sourceFrom || 0;
+			this.searchWord = searchWord || '';
 			if (product.isbn) {
 				this.getGoodQualityInfo(product.isbn);
 			}
 		},
+		buildAddCartParams(conditionType) {
+			const params = {
+				isbn: this.currentProduct.isbn,
+				quantity: this.quantity,
+				conditionType: conditionType,
+				sourceFrom: this.sourceFrom
+			};
+			if (this.searchWord) params.searchWord = this.searchWord;
+			return params;
+		},
 		//根据 isbn 获取商品品相信息
 		getGoodQualityInfo(isbn) {
 			uni.$u.http.get('/token/shop/bookDetail', { isbn }).then(res => {
@@ -234,12 +246,7 @@ export default {
 
 			this.isSubmitting = true; // 设置提交中状态
 
-			this.$u.api.addShopCartAjax({
-				isbn: this.currentProduct.isbn,
-				quantity: this.quantity,
-				conditionType: conditionType,
-				sourceFrom: this.sourceFrom
-			}).then(async res => {
+			this.$u.api.addShopCartAjax(this.buildAddCartParams(conditionType)).then(async res => {
 				if (res.code === 200) {
 					await this.$updateCartBadge();
 					this.$u.toast('加入购物车成功');
@@ -274,12 +281,7 @@ export default {
 
 			this.isSubmitting = true; // 设置提交中状态
 
-			this.$u.api.addShopCartAjax({
-				isbn: this.currentProduct.isbn,
-				quantity: this.quantity,
-				conditionType: conditionType,
-				sourceFrom: this.sourceFrom
-			}).then(async res => {
+			this.$u.api.addShopCartAjax(this.buildAddCartParams(conditionType)).then(async res => {
 				if (res.code === 200) {
 					await this.$updateCartBadge();
 					this.$u.toast('加入购物车成功');

+ 28 - 4
pages-sell/pages/detail.vue

@@ -126,6 +126,8 @@ export default {
             ],
             relatedBooksList: [],
             product: {},
+            entrySourceFrom: 0,
+            entrySearchWord: '',
             servicePosition: {
                 left: "auto",
                 right: 0,
@@ -134,6 +136,14 @@ export default {
         }
     },
     onLoad(options) {
+        this.entrySourceFrom = Number(options.sourceFrom) || 0;
+        if (options.searchWord) {
+            try {
+                this.entrySearchWord = decodeURIComponent(options.searchWord);
+            } catch (e) {
+                this.entrySearchWord = options.searchWord;
+            }
+        }
         let isbn = options.isbn || options.id; // Support both just in case
         
         // 兼容扫码进入场景,App.vue 可能会把 isbn 存入全局,或者当前页的 scene 里带了 isbn
@@ -204,7 +214,12 @@ export default {
             this.currentTab = index;
         },
         openSelectPopup(sourceFrom) {
-            this.$refs.selectPopup.open(this.product, sourceFrom);
+            const fromSearch = this.entrySourceFrom === 1;
+            this.$refs.selectPopup.open(
+                this.product,
+                fromSearch ? 1 : sourceFrom,
+                fromSearch ? this.entrySearchWord : ''
+            );
         },
         onNoticeChange(val) {
             this.$set(this.product, 'hasArrivalNotice', val);
@@ -246,11 +261,20 @@ export default {
                 return this.$u.toast('商品信息不完整');
             }
             const isCollect = this.product.isCollect;
-            const api = isCollect ? this.$u.api.removeCollectAjax : this.$u.api.addCollectAjax;
-            const params = isCollect ? [this.product.isbn] : this.product.isbn;
+            let request;
+            if (isCollect) {
+                request = this.$u.api.removeCollectAjax([this.product.isbn]);
+            } else {
+                const data = { isbn: this.product.isbn };
+                if (this.entrySourceFrom === 1) {
+                    data.sourceFrom = 1;
+                    if (this.entrySearchWord) data.searchWord = this.entrySearchWord;
+                }
+                request = this.$u.api.addCollectAjax(data);
+            }
 
             uni.showLoading({ mask: true });
-            api(params).then(res => {
+            request.then(res => {
                 uni.hideLoading();
                 if (res.code === 200) {
                     this.$set(this.product, 'isCollect', !isCollect);

+ 1 - 1
pages-sell/pages/recommend.vue

@@ -23,7 +23,7 @@
 
 		<!-- List Content -->
 		<view class="list-container">
-			<recommend-item v-for="(item, index) in bookList" :key="index" :item="item" :show-desc="cateType != 1" @update-item="updateBookItem($event, index)"></recommend-item>
+			<recommend-item v-for="(item, index) in bookList" :key="index" :item="item" :show-desc="cateType != 1" :source-from="2" @update-item="updateBookItem($event, index)"></recommend-item>
 			<u-loadmore :status="loadStatus" margin-top="30" margin-bottom="30"></u-loadmore>
 		</view>
 		<select-good-popup-host />

+ 11 - 2
pages-sell/pages/search-result.vue

@@ -32,7 +32,7 @@
             <!-- Everyone is watching -->
             <view class="section-block" v-if="hotBook && hotBook.isbn">
                 <text class="section-title">大家都在看</text>
-                <HotRecommendItem :item="hotBook" @add-cart="addToCart" @update-item="updateHotBook"></HotRecommendItem>
+                <HotRecommendItem :item="hotBook" :search-word="resultKeyword" @add-cart="addToCart" @update-item="updateHotBook"></HotRecommendItem>
             </view>
 
             <!-- Filter Bar -->
@@ -66,7 +66,8 @@
             <!-- Book List -->
             <view class="book-list">
                 <RecommendItem v-for="(item, index) in bookList" :key="index" :item="item" @add-cart="addToCart"
-                    @update-item="updateBookItem($event, index)" :show-desc="false">
+                    @update-item="updateBookItem($event, index)" :show-desc="false" :source-from="1"
+                    :search-word="resultKeyword">
                 </RecommendItem>
                 <u-loadmore :status="loadStatus" margin-top="30" margin-bottom="30"></u-loadmore>
             </view>
@@ -124,6 +125,7 @@ export default {
     data() {
         return {
             keyword: '',
+            resultKeyword: '',
             pageNum: 1,
             pageSize: 10,
             hotBook: {},
@@ -201,11 +203,18 @@ export default {
             if (this.minPrice) params.minPrice = this.minPrice;
             if (this.maxPrice) params.maxPrice = this.maxPrice;
 
+            const requestKeyword = params.keyword;
+            // 首屏列表还没出来时,「大家都在看」也可能先被加购,先记下本次搜索词
+            if (this.pageNum === 1 && !this.bookList.length) {
+                this.resultKeyword = requestKeyword;
+            }
             this.$u.api.getSearchKeywordAjax(params).then(res => {
                 uni.stopPullDownRefresh();
                 const rows = res.data.rows || [];
                 if (this.pageNum === 1) {
                     this.bookList = rows;
+                    // 用真正发起搜索的词做归因,避免输入框未提交的新词串到旧列表上
+                    this.resultKeyword = requestKeyword;
                 } else {
                     this.bookList = [...this.bookList, ...rows];
                 }

+ 1 - 1
pages-sell/pages/topic.vue

@@ -16,7 +16,7 @@
 		<!-- List Content -->
 		<view class="list-container">
 			<RecommendItem v-for="(item, index) in bookList" :key="index" :item="item" :show-desc="false"
-				@add-cart="handleAddToCart" @update-item="updateBookItem($event, index)"></RecommendItem>
+				:source-from="2" @add-cart="handleAddToCart" @update-item="updateBookItem($event, index)"></RecommendItem>
 
 			<!-- Change Batch Button -->
 			<view class="refresh-fab" @click="changeBatch" v-if="total > pageSize">