完善下单逻辑及其ui展示,修复支付倒计时显示错误bug
This commit is contained in:
@@ -1,35 +1,35 @@
|
||||
<template>
|
||||
<template>
|
||||
<view class="cart-search-page">
|
||||
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px', paddingRight: searchHeaderRightPadding + 'px' }">
|
||||
<view class="back-btn" @click="goBack">
|
||||
<text class="back-icon">‹</text>
|
||||
<text class="back-icon">鈥?/text>
|
||||
</view>
|
||||
|
||||
<view class="search-input-wrap">
|
||||
<text class="search-icon">⌕</text>
|
||||
<text class="search-icon">鈱?/text>
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="搜索购物车商品"
|
||||
placeholder="鎼滅储璐墿杞﹀晢鍝?
|
||||
confirm-type="search"
|
||||
:focus="true"
|
||||
@confirm="doSearch"
|
||||
/>
|
||||
<view v-if="keyword.length > 0" class="clear-keyword" @click="clearKeyword">
|
||||
<text class="clear-keyword-text">×</text>
|
||||
<text class="clear-keyword-text">脳</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="search-btn" @click="doSearch">
|
||||
<text class="search-btn-text">搜索</text>
|
||||
<text class="search-btn-text">鎼滅储</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view v-if="!hasSearched" class="search-content" :scroll-y="true" :show-scrollbar="false">
|
||||
<view class="history-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">历史搜索</text>
|
||||
<text class="clear-history" @click="clearSearchHistory">清空</text>
|
||||
<text class="section-title">鍘嗗彶鎼滅储</text>
|
||||
<text class="clear-history" @click="clearSearchHistory">娓呯┖</text>
|
||||
</view>
|
||||
|
||||
<view v-if="searchHistory.length > 0" class="history-list">
|
||||
@@ -44,13 +44,13 @@
|
||||
</view>
|
||||
|
||||
<view v-else class="empty-history">
|
||||
<text class="empty-history-text">暂无历史搜索</text>
|
||||
<text class="empty-history-text">鏆傛棤鍘嗗彶鎼滅储</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="discover-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索发现</text>
|
||||
<text class="section-title">鎼滅储鍙戠幇</text>
|
||||
</view>
|
||||
|
||||
<view class="discover-grid">
|
||||
@@ -66,10 +66,10 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view v-else class="search-result-content" :scroll-y="true" :show-scrollbar="false">
|
||||
<scroll-view v-else class="search-result-content" :scroll-y="true" :show-scrollbar="false" :lower-threshold="120" @scrolltolower="handleResultScrollToLower">
|
||||
<view v-if="matchedCartItems.length > 0" class="cart-match-section">
|
||||
<view class="result-section-title-wrap">
|
||||
<text class="result-section-title">购物车内相关商品</text>
|
||||
<text class="result-section-title">璐墿杞﹀唴鐩稿叧鍟嗗搧</text>
|
||||
</view>
|
||||
|
||||
<view class="cart-result-list">
|
||||
@@ -79,7 +79,7 @@
|
||||
class="cart-result-card"
|
||||
>
|
||||
<view class="item-select" @click="toggleSelect(item.id)">
|
||||
<text v-if="item.selected" class="selected-icon">✓</text>
|
||||
<text v-if="item.selected" class="selected-icon">鉁?/text>
|
||||
<text v-else class="unselected-icon"></text>
|
||||
</view>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<text class="item-name" :lines="1">{{ item.name }}</text>
|
||||
<text class="item-spec">{{ item.spec }}</text>
|
||||
<view class="item-footer">
|
||||
<text class="item-price">¥{{ item.price }}</text>
|
||||
<text class="item-price">楼{{ item.price }}</text>
|
||||
<view class="quantity-control">
|
||||
<text class="quantity-btn" @click="decreaseQuantity(item.id)">-</text>
|
||||
<text class="quantity-value">{{ item.quantity }}</text>
|
||||
@@ -112,31 +112,17 @@
|
||||
<view class="recommend-search-section">
|
||||
<view class="recommend-title-wrap">
|
||||
<view class="line"></view>
|
||||
<text class="recommend-title">{{ matchedCartItems.length > 0 ? '为你搜索全站商品' : '为你搜索全部商品' }}</text>
|
||||
<text class="recommend-title">{{ matchedCartItems.length > 0 ? '涓轰綘鎼滅储鍏ㄧ珯鍟嗗搧' : '涓轰綘鎼滅储鍏ㄩ儴鍟嗗搧' }}</text>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
|
||||
<view class="recommend-grid">
|
||||
<view
|
||||
v-for="product in recommendProducts"
|
||||
:key="product.id"
|
||||
class="recommend-card"
|
||||
@click="goToProductDetail(product)"
|
||||
>
|
||||
<image class="recommend-image" :src="product.image" mode="aspectFill" />
|
||||
<view class="recommend-info">
|
||||
<text class="recommend-shop-tag">{{ product.shopName }}</text>
|
||||
<text class="recommend-name" :lines="2">{{ product.name }}</text>
|
||||
<text class="recommend-sales">{{ product.salesText }}</text>
|
||||
<view class="recommend-price-row">
|
||||
<text class="recommend-price">¥{{ product.price }}</text>
|
||||
<view class="recommend-cart-btn" @click.stop="addRecommendToCart(product)">
|
||||
<text class="recommend-cart-icon">🛒</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<GuessYouLike
|
||||
title="猜你喜欢"
|
||||
:pageSize="8"
|
||||
:excludeProductIds="matchedProductIds"
|
||||
:loadMoreKey="guessLoadMoreKey"
|
||||
@productClick="goToRecommendProductDetail"
|
||||
/>
|
||||
|
||||
<view class="bottom-safe-space"></view>
|
||||
</view>
|
||||
@@ -146,19 +132,19 @@
|
||||
<view class="settlement-inner">
|
||||
<view class="settlement-left" @click="toggleSelectAllInSearch">
|
||||
<view class="select-circle" :class="{ 'select-circle-active': allSearchSelected }">
|
||||
<text v-if="allSearchSelected" class="select-check">✓</text>
|
||||
<text v-if="allSearchSelected" class="select-check">鉁?/text>
|
||||
</view>
|
||||
<text class="select-all-text">全选</text>
|
||||
<text class="select-all-text">鍏ㄩ€?/text>
|
||||
</view>
|
||||
|
||||
<view class="settlement-right">
|
||||
<view class="total-info">
|
||||
<text class="total-label">合计:</text>
|
||||
<text class="total-price">¥{{ searchTotalPrice }}</text>
|
||||
<text class="total-label">鍚堣:</text>
|
||||
<text class="total-price">楼{{ searchTotalPrice }}</text>
|
||||
</view>
|
||||
|
||||
<button class="checkout-btn" :class="{ 'checkout-btn-disabled': searchSelectedCount == 0 }" @click="goToCheckoutFromSearch">
|
||||
去结算({{ searchSelectedCount }})
|
||||
鍘荤粨绠?{{ searchSelectedCount }})
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -171,6 +157,7 @@ import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
|
||||
import { goToLogin } from '@/utils/utils.uts'
|
||||
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
|
||||
|
||||
const CART_SEARCH_HISTORY_KEY = 'cart_search_history'
|
||||
|
||||
@@ -211,19 +198,19 @@ const keyword = ref<string>('')
|
||||
const hasSearched = ref<boolean>(false)
|
||||
const searchHistory = ref<Array<string>>([])
|
||||
const searchDiscoverList = ref<Array<string>>([
|
||||
'无人机',
|
||||
'水杯',
|
||||
'手机',
|
||||
'天然泉水',
|
||||
'按摩仪',
|
||||
'摄像头',
|
||||
'耳机',
|
||||
'停车场设备',
|
||||
'饮料',
|
||||
'鏃犱汉鏈?,
|
||||
'姘存澂',
|
||||
'鎵嬫満',
|
||||
'澶╃劧娉夋按',
|
||||
'鎸夋懇浠?,
|
||||
'鎽勫儚澶?,
|
||||
'鑰虫満',
|
||||
'鍋滆溅鍦鸿澶?,
|
||||
'楗枡',
|
||||
'iPhone'
|
||||
])
|
||||
const cartItems = ref<Array<LocalCartItem>>([])
|
||||
const recommendProducts = ref<Array<ProductItem>>([])
|
||||
const guessLoadMoreKey = ref<number>(0)
|
||||
const isLoading = ref<boolean>(false)
|
||||
const statusBarHeight = ref<number>(0)
|
||||
const updatingItems = ref<Set<string>>(new Set())
|
||||
@@ -233,7 +220,7 @@ const searchDiscoverWords = computed<Array<string>>(() => {
|
||||
if (searchDiscoverList.value.length > 0) {
|
||||
return searchDiscoverList.value
|
||||
}
|
||||
return ['无人机', '水杯', '手机', '天然泉水']
|
||||
return ['鏃犱汉鏈?, '姘存澂', '鎵嬫満', '澶╃劧娉夋按']
|
||||
})
|
||||
|
||||
const safeLower = (value: string): string => {
|
||||
@@ -246,8 +233,8 @@ const matchedCartItems = computed<Array<LocalCartItem>>(() => {
|
||||
return []
|
||||
}
|
||||
|
||||
return cartItems.value.filter((item: LocalCartItem) => {
|
||||
const title = safeLower(item.name)
|
||||
return cartItems.value.filter((item: LocalCartItem) => {
|
||||
const title = safeLower(item.name)
|
||||
const name = safeLower(item.name)
|
||||
const productName = safeLower(item.productName)
|
||||
const skuName = safeLower(item.skuName)
|
||||
@@ -256,17 +243,28 @@ const matchedCartItems = computed<Array<LocalCartItem>>(() => {
|
||||
const merchantName = safeLower(item.merchantName)
|
||||
const brandName = safeLower(item.brandName)
|
||||
|
||||
return title.indexOf(q) >= 0
|
||||
|| name.indexOf(q) >= 0
|
||||
|| productName.indexOf(q) >= 0
|
||||
|| skuName.indexOf(q) >= 0
|
||||
|| specName.indexOf(q) >= 0
|
||||
|| shopName.indexOf(q) >= 0
|
||||
|| merchantName.indexOf(q) >= 0
|
||||
|| brandName.indexOf(q) >= 0
|
||||
return title.indexOf(q) >= 0
|
||||
|| name.indexOf(q) >= 0
|
||||
|| productName.indexOf(q) >= 0
|
||||
|| skuName.indexOf(q) >= 0
|
||||
|| specName.indexOf(q) >= 0
|
||||
|| shopName.indexOf(q) >= 0
|
||||
|| merchantName.indexOf(q) >= 0
|
||||
|| brandName.indexOf(q) >= 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const matchedProductIds = computed<Array<string>>(() => {
|
||||
const ids: Array<string> = []
|
||||
for (let i = 0; i < matchedCartItems.value.length; i++) {
|
||||
const item = matchedCartItems.value[i]
|
||||
const productId = item.productId !== '' ? item.productId : item.id
|
||||
if (productId !== '' && ids.indexOf(productId) < 0) {
|
||||
ids.push(productId)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
})
|
||||
const searchSelectedItems = computed<Array<LocalCartItem>>(() => {
|
||||
return matchedCartItems.value.filter((item: LocalCartItem) => item.selected == true)
|
||||
})
|
||||
@@ -291,9 +289,9 @@ const allSearchSelected = computed((): boolean => {
|
||||
|
||||
const noCartResultText = computed((): string => {
|
||||
if (cartItems.value.length == 0) {
|
||||
return '购物车为空,暂无相关商品'
|
||||
return '璐墿杞︿负绌猴紝鏆傛棤鐩稿叧鍟嗗搧'
|
||||
}
|
||||
return '您的购物车里没有相关商品'
|
||||
return '鎮ㄧ殑璐墿杞﹂噷娌℃湁鐩稿叧鍟嗗搧'
|
||||
})
|
||||
|
||||
const searchHeaderRightPadding = computed((): number => {
|
||||
@@ -326,7 +324,7 @@ const loadSearchHistory = () => {
|
||||
const parsed = JSON.parse(cache) as Array<string>
|
||||
searchHistory.value = parsed
|
||||
} catch (e) {
|
||||
console.error('解析搜索历史失败:', e)
|
||||
console.error('瑙f瀽鎼滅储鍘嗗彶澶辫触:', e)
|
||||
searchHistory.value = []
|
||||
}
|
||||
}
|
||||
@@ -352,81 +350,6 @@ const clearSearchHistory = () => {
|
||||
uni.removeStorageSync(CART_SEARCH_HISTORY_KEY)
|
||||
}
|
||||
|
||||
const mockRecommendProducts = (q: string): Array<ProductItem> => {
|
||||
const text = q.trim() == '' ? '热卖好物' : q
|
||||
return [
|
||||
{
|
||||
id: 'mock-1',
|
||||
name: text + ' 便携款',
|
||||
price: 99,
|
||||
image: '/static/images/default.png',
|
||||
shopName: '平台精选',
|
||||
salesText: '已售 200+',
|
||||
merchantId: '',
|
||||
skuId: ''
|
||||
},
|
||||
{
|
||||
id: 'mock-2',
|
||||
name: text + ' 升级版',
|
||||
price: 159,
|
||||
image: '/static/images/default.png',
|
||||
shopName: '品牌旗舰',
|
||||
salesText: '好评 98%',
|
||||
merchantId: '',
|
||||
skuId: ''
|
||||
},
|
||||
{
|
||||
id: 'mock-3',
|
||||
name: text + ' 热销套装',
|
||||
price: 239,
|
||||
image: '/static/images/default.png',
|
||||
shopName: '今日推荐',
|
||||
salesText: '月销 500+',
|
||||
merchantId: '',
|
||||
skuId: ''
|
||||
},
|
||||
{
|
||||
id: 'mock-4',
|
||||
name: text + ' 家用精选',
|
||||
price: 79,
|
||||
image: '/static/images/default.png',
|
||||
shopName: '官方自营',
|
||||
salesText: '已售 1200+',
|
||||
merchantId: '',
|
||||
skuId: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const loadRecommendProducts = async (q: string) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const result = await supabaseService.searchProducts(q, 1, 8, 'sales')
|
||||
if (result.data.length > 0) {
|
||||
recommendProducts.value = result.data.map((product: Product): ProductItem => {
|
||||
const saleCount = product.sale_count ?? product.sales ?? 0
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.base_price ?? product.market_price ?? 0,
|
||||
image: product.main_image_url ?? product.image_url ?? '/static/images/default.png',
|
||||
shopName: product.shop_name ?? '平台精选',
|
||||
salesText: '已售 ' + saleCount + '+',
|
||||
merchantId: product.merchant_id ?? '',
|
||||
skuId: ''
|
||||
}
|
||||
})
|
||||
} else {
|
||||
recommendProducts.value = mockRecommendProducts(q)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载推荐商品失败:', e)
|
||||
recommendProducts.value = mockRecommendProducts(q)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadCartData = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
@@ -438,7 +361,7 @@ const loadCartData = async () => {
|
||||
memberDiscount = discountRaw as number
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('获取会员信息失败,使用默认折扣:', e)
|
||||
console.log('鑾峰彇浼氬憳淇℃伅澶辫触锛屼娇鐢ㄩ粯璁ゆ姌鎵?', e)
|
||||
}
|
||||
|
||||
const supabaseCartItems = await supabaseService.getCartItems()
|
||||
@@ -449,18 +372,18 @@ const loadCartData = async () => {
|
||||
memberPrice = Math.round(originalPrice * memberDiscount * 100) / 100
|
||||
}
|
||||
|
||||
const productName = item.product_name ?? '未知商品'
|
||||
const specName = item.product_specification ?? '标准规格'
|
||||
const productName = item.product_name ?? '鏈煡鍟嗗搧'
|
||||
const specName = item.product_specification ?? '鏍囧噯瑙勬牸'
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
shopId: item.shop_id ?? 'default_shop',
|
||||
shopName: item.shop_name ?? '商城优选',
|
||||
shopName: item.shop_name ?? '鍟嗗煄浼橀€?,
|
||||
name: productName,
|
||||
productName: productName,
|
||||
skuName: specName,
|
||||
specName: specName,
|
||||
merchantName: item.shop_name ?? '商城优选',
|
||||
merchantName: item.shop_name ?? '鍟嗗煄浼橀€?,
|
||||
brandName: '',
|
||||
price: originalPrice,
|
||||
originalPrice: originalPrice,
|
||||
@@ -475,7 +398,7 @@ const loadCartData = async () => {
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载购物车搜索数据失败:', e)
|
||||
console.error('鍔犺浇璐墿杞︽悳绱㈡暟鎹け璐?', e)
|
||||
cartItems.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -486,7 +409,7 @@ const doSearch = async () => {
|
||||
const q = keyword.value.trim()
|
||||
if (q == '') {
|
||||
uni.showToast({
|
||||
title: '请输入搜索关键词',
|
||||
title: '璇疯緭鍏ユ悳绱㈠叧閿瘝',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
@@ -494,7 +417,6 @@ const doSearch = async () => {
|
||||
|
||||
hasSearched.value = true
|
||||
saveSearchHistory(q)
|
||||
await loadRecommendProducts(q)
|
||||
}
|
||||
|
||||
const useSearchWord = (word: string) => {
|
||||
@@ -509,7 +431,7 @@ const goBack = () => {
|
||||
const clearKeyword = () => {
|
||||
keyword.value = ''
|
||||
hasSearched.value = false
|
||||
recommendProducts.value = []
|
||||
guessLoadMoreKey.value = 0
|
||||
}
|
||||
|
||||
const toggleSelect = async (itemId: string) => {
|
||||
@@ -524,7 +446,7 @@ const toggleSelect = async (itemId: string) => {
|
||||
if (!success) {
|
||||
cartItems.value[index].selected = !newSelected
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
uni.showToast({ title: '缃戠粶寮傚父锛岃閲嶈瘯', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,7 +465,7 @@ const increaseQuantity = async (itemId: string) => {
|
||||
if (!success) {
|
||||
cartItems.value[index].quantity = newQuantity - 1
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
uni.showToast({ title: '鏇存柊澶辫触', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,7 +475,7 @@ const decreaseQuantity = async (itemId: string) => {
|
||||
if (index == -1) return
|
||||
|
||||
if (cartItems.value[index].quantity <= 1) {
|
||||
uni.showToast({ title: '最少保留1件,可返回购物车删除', icon: 'none' })
|
||||
uni.showToast({ title: '鏈€灏戜繚鐣?浠讹紝鍙繑鍥炶喘鐗╄溅鍒犻櫎', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -567,7 +489,7 @@ const decreaseQuantity = async (itemId: string) => {
|
||||
if (!success) {
|
||||
cartItems.value[index].quantity = newQuantity + 1
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '更新失败', icon: 'none' })
|
||||
uni.showToast({ title: '鏇存柊澶辫触', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,14 +509,14 @@ const toggleSelectAllInSearch = async () => {
|
||||
item.selected = !checked
|
||||
})
|
||||
cartItems.value = [...cartItems.value]
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
uni.showToast({ title: '鎿嶄綔澶辫触', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const goToCheckoutFromSearch = () => {
|
||||
if (searchSelectedCount.value == 0) {
|
||||
uni.showToast({
|
||||
title: '请选择商品',
|
||||
title: '璇烽€夋嫨鍟嗗搧',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
@@ -620,8 +542,8 @@ const goToCheckoutFromSearch = () => {
|
||||
try {
|
||||
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
|
||||
} catch (e) {
|
||||
console.error('存储结算数据失败', e)
|
||||
uni.showToast({ title: '系统异常,请重试', icon: 'none' })
|
||||
console.error('瀛樺偍缁撶畻鏁版嵁澶辫触', e)
|
||||
uni.showToast({ title: '绯荤粺寮傚父锛岃閲嶈瘯', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -653,38 +575,17 @@ const goToProductDetail = (product: ProductItem) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
const addRecommendToCart = async (product: ProductItem) => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == null || userId == '') {
|
||||
goToLogin('/pages/main/cart-search/cart-search')
|
||||
const goToRecommendProductDetail = (productId: string) => {
|
||||
if (productId === '') {
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/product-detail?id=' + encodeURIComponent(productId) + '&productId=' + encodeURIComponent(productId)
|
||||
})
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
try {
|
||||
const skus = await supabaseService.getProductSkus(product.id)
|
||||
if (skus.length > 0) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '请选择规格', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
goToProductDetail(product)
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
|
||||
const success = await supabaseService.addToCart(product.id, 1, product.skuId, product.merchantId)
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||
loadCartData()
|
||||
} else {
|
||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('推荐商品加入购物车失败:', e)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||
}
|
||||
const handleResultScrollToLower = () => {
|
||||
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
|
||||
}
|
||||
|
||||
onLoad((options: UTSJSONObject) => {
|
||||
@@ -698,7 +599,7 @@ onLoad((options: UTSJSONObject) => {
|
||||
navBarRight.value = (systemInfo.screenWidth - menuButton.left) + 10
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('获取胶囊按钮信息失败:', e)
|
||||
console.log('鑾峰彇鑳跺泭鎸夐挳淇℃伅澶辫触:', e)
|
||||
navBarRight.value = 96
|
||||
}
|
||||
// #endif
|
||||
@@ -1304,4 +1205,4 @@ onLoad((options: UTSJSONObject) => {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -174,40 +174,13 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐商品 -->
|
||||
<view v-if="currentCartType == 'goods' && recommendProducts.length > 0" class="recommend-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">猜你喜欢</text>
|
||||
</view>
|
||||
<view class="recommend-list">
|
||||
<view
|
||||
v-for="product in recommendProducts"
|
||||
:key="product.id"
|
||||
class="recommend-item"
|
||||
@click="navigateToProduct(product)"
|
||||
>
|
||||
<view class="recommend-image-wrapper">
|
||||
<image
|
||||
class="recommend-image"
|
||||
:src="product.image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<text class="recommend-name" :lines="2">{{ product.name }}</text>
|
||||
<view class="recommend-bottom">
|
||||
<text class="recommend-price">¥{{ product.price }}</text>
|
||||
<view class="recommend-add-btn" @click.stop="addToCart(product)">
|
||||
<text class="recommend-add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="recommend-load-more" @click="loadRecommendProducts(false)">
|
||||
<text v-if="recommendLoading" class="recommend-load-text">正在加载更多...</text>
|
||||
<text v-else-if="!recommendHasMore && recommendInitialized" class="recommend-load-text">没有更多了</text>
|
||||
<text v-else class="recommend-load-text">上拉加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
<GuessYouLike
|
||||
v-if="currentCartType == 'goods'"
|
||||
title="猜你喜欢"
|
||||
:pageSize="8"
|
||||
:loadMoreKey="guessLoadMoreKey"
|
||||
@productClick="handleGuessProductClick"
|
||||
/>
|
||||
<!-- 底部占位符:确保内容不被原生 TabBar 遮挡 -->
|
||||
<view class="tabbar-safe-area"></view>
|
||||
</scroll-view>
|
||||
@@ -280,6 +253,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { supabaseService, type CartItem as SupabaseCartItem, type Product } from '@/utils/supabaseService.uts'
|
||||
import { goToLogin } from '@/utils/utils.uts'
|
||||
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
|
||||
|
||||
type ModalSuccess = { confirm: boolean; cancel: boolean }
|
||||
|
||||
@@ -375,6 +349,7 @@ const recommendBottomLocked = ref<boolean>(false)
|
||||
const recommendViewportHeight = ref<number>(0)
|
||||
const pageWindowHeight = ref<number>(0)
|
||||
const isAndroidApp = ref<boolean>(false)
|
||||
const guessLoadMoreKey = ref<number>(0)
|
||||
const serviceMockItems = ref<ServiceCartItem[]>([
|
||||
{
|
||||
id: 'service-1',
|
||||
@@ -725,27 +700,6 @@ const mergeRecommendProducts = (oldList: RecommendProduct[], newList: RecommendP
|
||||
return result
|
||||
}
|
||||
|
||||
const mockRecommendProducts = (page: number, pageSize: number): RecommendProduct[] => {
|
||||
const list: RecommendProduct[] = []
|
||||
if (page >= 5) {
|
||||
return list
|
||||
}
|
||||
for (let i = 0; i < pageSize; i++) {
|
||||
const index = (page - 1) * pageSize + i + 1
|
||||
list.push({
|
||||
id: 'mock-recommend-' + index,
|
||||
shopId: 'mock-shop-' + index,
|
||||
shopName: '商城推荐',
|
||||
name: '猜你喜欢 推荐商品 ' + index,
|
||||
price: 9.9 + index,
|
||||
image: '/static/images/default.png',
|
||||
skuId: '',
|
||||
merchant_id: ''
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const fetchRecommendProducts = async (page: number, pageSize: number): Promise<RecommendProduct[]> => {
|
||||
console.log('[cart推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
|
||||
const hotResp = await supabaseService.searchProducts('', page, pageSize, 'sales')
|
||||
@@ -851,13 +805,7 @@ async function loadRecommendProducts(reset: boolean): Promise<void> {
|
||||
recommendInitialized.value = true
|
||||
} catch (error) {
|
||||
console.error('加载推荐商品失败:', error)
|
||||
if (reset && recommendProducts.value.length === 0) {
|
||||
const mockList = mockRecommendProducts(1, recommendPageSize.value)
|
||||
recommendProducts.value = mockList
|
||||
recommendPage.value = 2
|
||||
recommendHasMore.value = true
|
||||
recommendInitialized.value = true
|
||||
}
|
||||
recommendHasMore.value = false
|
||||
} finally {
|
||||
recommendLoading.value = false
|
||||
if (!reset && recommendPendingLoad.value && recommendHasMore.value) {
|
||||
@@ -869,59 +817,13 @@ async function loadRecommendProducts(reset: boolean): Promise<void> {
|
||||
}
|
||||
|
||||
function onRecommendScrollToLower(): void {
|
||||
console.log('[cart推荐] scrolltolower 触发 currentCartType=', currentCartType.value, 'initialized=', recommendInitialized.value)
|
||||
if (currentCartType.value != 'goods') {
|
||||
console.log('[cart推荐] 跳过:当前不是 goods')
|
||||
return
|
||||
if (currentCartType.value == 'goods') {
|
||||
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
|
||||
}
|
||||
recommendBottomLocked.value = true
|
||||
loadRecommendProducts(false)
|
||||
}
|
||||
|
||||
function onRecommendScroll(event: any): void {
|
||||
if (currentCartType.value != 'goods' || recommendLoading.value || !recommendHasMore.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const eventObj = toRecommendScrollJson(event)
|
||||
let detailObj: UTSJSONObject | null = null
|
||||
if (eventObj != null) {
|
||||
detailObj = toRecommendScrollJson(eventObj.get('detail'))
|
||||
}
|
||||
if (detailObj == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = readRecommendScrollMetric(detailObj, 'scrollTop')
|
||||
const scrollHeight = readRecommendScrollMetric(detailObj, 'scrollHeight')
|
||||
let clientHeight = readRecommendScrollMetric(detailObj, 'clientHeight')
|
||||
|
||||
if (clientHeight <= 0) {
|
||||
clientHeight = recommendViewportHeight.value
|
||||
}
|
||||
|
||||
console.log('[cart推荐] scroll事件 scrollTop=', scrollTop, 'scrollHeight=', scrollHeight, 'clientHeight=', clientHeight)
|
||||
|
||||
if (scrollHeight <= 0 || clientHeight <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const distanceToBottom = scrollHeight - scrollTop - clientHeight
|
||||
if (distanceToBottom > 260) {
|
||||
recommendBottomLocked.value = false
|
||||
}
|
||||
if (distanceToBottom <= 180) {
|
||||
console.log('[cart推荐] scroll 兜底触底 distanceToBottom=', distanceToBottom)
|
||||
if (recommendBottomLocked.value) {
|
||||
recommendPendingLoad.value = true
|
||||
return
|
||||
}
|
||||
recommendBottomLocked.value = true
|
||||
loadRecommendProducts(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[cart推荐] 处理推荐滚动失败:', e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
@@ -981,9 +883,6 @@ const loadCartData = async () => {
|
||||
console.log('Transformed items count:', transformedItems.length);
|
||||
cartItems.value = transformedItems
|
||||
|
||||
if (!recommendInitialized.value) {
|
||||
await loadRecommendProducts(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载购物车数据失败:', error)
|
||||
cartItems.value = []
|
||||
@@ -996,6 +895,10 @@ onShow(() => {
|
||||
loadCartData()
|
||||
})
|
||||
|
||||
const handleGuessProductClick = (productId: string) => {
|
||||
navigateToProduct({ id: productId, productId: productId, price: 0 })
|
||||
}
|
||||
|
||||
// 商品操作 - 更新选中状态到Supabase
|
||||
const toggleSelect = async (itemId: string) => {
|
||||
// 乐观更新
|
||||
@@ -2593,4 +2496,3 @@ const goToCheckout = () => {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -436,68 +436,6 @@ function buildServiceImageText(categoryId: string): string {
|
||||
return '服'
|
||||
}
|
||||
|
||||
function buildMockServiceProducts(): Array<HomeCareServiceProductType> {
|
||||
// TODO: 后续替换为服务首页专用接口,当前仅在真实服务目录为空时兜底。
|
||||
return [
|
||||
{
|
||||
id: 'svc-001',
|
||||
title: '基础上门照护',
|
||||
subtitle: '协助起居、日常陪护、健康观察',
|
||||
categoryId: 'basic_care',
|
||||
price: 99,
|
||||
unit: '次',
|
||||
tags: ['平台认证', '可预约'],
|
||||
salesText: '已服务230+',
|
||||
imageText: '护',
|
||||
coverGradient: getServiceGradient('basic_care'),
|
||||
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001',
|
||||
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001&mode=booking'
|
||||
},
|
||||
{
|
||||
id: 'svc-002',
|
||||
title: '居家康复指导',
|
||||
subtitle: '术后恢复、动作训练、康复评估',
|
||||
categoryId: 'rehab',
|
||||
price: 129,
|
||||
unit: '次',
|
||||
tags: ['康复指导', '上门服务'],
|
||||
salesText: '已服务180+',
|
||||
imageText: '康',
|
||||
coverGradient: getServiceGradient('rehab'),
|
||||
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002',
|
||||
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002&mode=booking'
|
||||
},
|
||||
{
|
||||
id: 'svc-mock-escort',
|
||||
title: '陪诊陪护服务',
|
||||
subtitle: '挂号陪同、检查陪同、取药协助',
|
||||
categoryId: 'escort',
|
||||
price: 168,
|
||||
unit: '次',
|
||||
tags: ['陪诊服务', '安心陪护'],
|
||||
salesText: '已服务320+',
|
||||
imageText: '陪',
|
||||
coverGradient: getServiceGradient('escort'),
|
||||
detailPath: '',
|
||||
bookingPath: ''
|
||||
},
|
||||
{
|
||||
id: 'svc-003',
|
||||
title: '慢病随访服务',
|
||||
subtitle: '血压血糖记录、健康建议、定期回访',
|
||||
categoryId: 'chronic',
|
||||
price: 79,
|
||||
unit: '次',
|
||||
tags: ['慢病管理', '健康随访'],
|
||||
salesText: '已服务150+',
|
||||
imageText: '访',
|
||||
coverGradient: getServiceGradient('chronic'),
|
||||
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003',
|
||||
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003&mode=booking'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function buildServiceProductsFromCatalog(catalog: Array<HomeServiceCatalogType>): Array<HomeCareServiceProductType> {
|
||||
const result: Array<HomeCareServiceProductType> = []
|
||||
for (let i = 0; i < catalog.length; i++) {
|
||||
@@ -525,14 +463,10 @@ async function loadServiceHomeData(): Promise<void> {
|
||||
serviceLoading.value = true
|
||||
try {
|
||||
const catalog = await fetchHomeServiceCatalog()
|
||||
if (catalog.length > 0) {
|
||||
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
|
||||
} else {
|
||||
allServiceProducts.value = buildMockServiceProducts()
|
||||
}
|
||||
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
|
||||
} catch (error) {
|
||||
console.error('加载服务首页数据失败', error)
|
||||
allServiceProducts.value = buildMockServiceProducts()
|
||||
allServiceProducts.value = [] as Array<HomeCareServiceProductType>
|
||||
} finally {
|
||||
serviceLoading.value = false
|
||||
}
|
||||
@@ -620,6 +554,8 @@ const hotProducts = ref<Product[]>([])
|
||||
const recommendedProducts = ref<Product[]>([])
|
||||
const hotKeywords = ref<string[]>([])
|
||||
const defaultLoadLimit: number = 6
|
||||
const recommendChannelLoadLimit: number = 16
|
||||
const categoryChannelLoadLimit: number = 12
|
||||
|
||||
// 屏幕尺寸检测
|
||||
const isMobile = ref(false)
|
||||
@@ -922,10 +858,376 @@ function buildSimpleChannelCoverImages(startIndex: number): string[] {
|
||||
return covers
|
||||
}
|
||||
|
||||
function buildSimpleCategoryChannels(categoryId: string): SimpleCategoryChannel[] {
|
||||
return []
|
||||
function getRealProductImage(product: Product): string {
|
||||
if (product.main_image_url != null && product.main_image_url !== '') {
|
||||
return product.main_image_url
|
||||
}
|
||||
if (product.images != null && product.images.length > 0 && product.images[0] !== '') {
|
||||
return product.images[0]
|
||||
}
|
||||
if (product.image_url != null && product.image_url !== '') {
|
||||
return product.image_url
|
||||
}
|
||||
return '/static/images/default.png'
|
||||
}
|
||||
|
||||
function getRealSalePrice(product: Product): number {
|
||||
return product.base_price ?? product.price ?? 0
|
||||
}
|
||||
|
||||
|
||||
function getRealMarketPrice(product: Product): number {
|
||||
return product.market_price ?? product.original_price ?? 0
|
||||
}
|
||||
|
||||
function toChannelProduct(product: Product, labelPrefix: string): ChannelProduct {
|
||||
const salePrice = getRealSalePrice(product)
|
||||
const marketPrice = getRealMarketPrice(product)
|
||||
const shortName = product.short_title != null && product.short_title !== ''
|
||||
? product.short_title
|
||||
: (product.name != null && product.name !== '' ? product.name : product.id)
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name != null && product.name !== '' ? product.name : product.id,
|
||||
shortName,
|
||||
image: getRealProductImage(product),
|
||||
price: salePrice,
|
||||
marketPrice,
|
||||
tag: labelPrefix
|
||||
} as ChannelProduct
|
||||
}
|
||||
|
||||
function getProductDiscountScore(product: Product): number {
|
||||
const salePrice = getRealSalePrice(product)
|
||||
const marketPrice = getRealMarketPrice(product)
|
||||
if (marketPrice <= salePrice || marketPrice <= 0) {
|
||||
return 0
|
||||
}
|
||||
const discountValue = marketPrice - salePrice
|
||||
const discountRate = discountValue / marketPrice
|
||||
return discountRate * 100000 + discountValue
|
||||
}
|
||||
|
||||
function getProductQualityScore(product: Product): number {
|
||||
let score = 0
|
||||
if (product.is_featured == true) {
|
||||
score = score + 100000
|
||||
}
|
||||
if (product.is_hot == true) {
|
||||
score = score + 50000
|
||||
}
|
||||
score = score + (product.sale_count ?? 0)
|
||||
return score
|
||||
}
|
||||
|
||||
function getProductHotScore(product: Product): number {
|
||||
let score = product.sale_count ?? 0
|
||||
if (product.is_hot == true) {
|
||||
score = score + 100000
|
||||
}
|
||||
if (product.is_featured == true) {
|
||||
score = score + 50000
|
||||
}
|
||||
score = score + getProductDiscountScore(product)
|
||||
return score
|
||||
}
|
||||
|
||||
function cloneProductArray(source: Array<Product>): Array<Product> {
|
||||
const result: Array<Product> = []
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
result.push(source[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function sortProductsByScoreDesc(source: Array<Product>, scoreType: string): Array<Product> {
|
||||
const result = cloneProductArray(source)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
let leftScore = 0
|
||||
let rightScore = 0
|
||||
if (scoreType == 'discount') {
|
||||
leftScore = getProductDiscountScore(result[i])
|
||||
rightScore = getProductDiscountScore(result[j])
|
||||
} else if (scoreType == 'quality') {
|
||||
leftScore = getProductQualityScore(result[i])
|
||||
rightScore = getProductQualityScore(result[j])
|
||||
} else {
|
||||
leftScore = getProductHotScore(result[i])
|
||||
rightScore = getProductHotScore(result[j])
|
||||
}
|
||||
if (rightScore > leftScore) {
|
||||
const temp = result[i]
|
||||
result[i] = result[j]
|
||||
result[j] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function sortProductsByPriceAsc(source: Array<Product>): Array<Product> {
|
||||
const result = cloneProductArray(source)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const leftPrice = getRealSalePrice(result[i])
|
||||
const rightPrice = getRealSalePrice(result[j])
|
||||
if (rightPrice < leftPrice) {
|
||||
const temp = result[i]
|
||||
result[i] = result[j]
|
||||
result[j] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function filterProductsByMode(source: Array<Product>, mode: string): Array<Product> {
|
||||
const result: Array<Product> = []
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const item = source[i]
|
||||
const salePrice = getRealSalePrice(item)
|
||||
const marketPrice = getRealMarketPrice(item)
|
||||
if (mode == 'discount' && marketPrice > salePrice) {
|
||||
result.push(item)
|
||||
continue
|
||||
}
|
||||
if (mode == 'quality' && (item.is_featured == true || item.is_hot == true)) {
|
||||
result.push(item)
|
||||
continue
|
||||
}
|
||||
if (mode == 'cheap-9' && salePrice > 0 && salePrice <= 9.9) {
|
||||
result.push(item)
|
||||
continue
|
||||
}
|
||||
if (mode == 'cheap-19' && salePrice > 0 && salePrice <= 19.9) {
|
||||
result.push(item)
|
||||
continue
|
||||
}
|
||||
if (mode == 'live' && (item.is_hot == true || (item.sale_count ?? 0) > 0)) {
|
||||
result.push(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function mergeUniqueProductLists(first: Array<Product>, second: Array<Product>, third: Array<Product>): Array<Product> {
|
||||
const result: Array<Product> = []
|
||||
const seenIds: Array<string> = []
|
||||
const sources: Array<Array<Product>> = [first, second, third]
|
||||
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
|
||||
const source = sources[sourceIndex]
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const item = source[i]
|
||||
const productId = item.id ?? ''
|
||||
if (productId != '' && seenIds.indexOf(productId) != -1) {
|
||||
continue
|
||||
}
|
||||
if (productId != '') {
|
||||
seenIds.push(productId)
|
||||
}
|
||||
result.push(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function appendChannelProducts(source: Array<Product>, result: Array<Product>, selectedIds: Array<string>, desiredCount: number, allowRepeat: boolean): void {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (result.length >= desiredCount) {
|
||||
return
|
||||
}
|
||||
const item = source[i]
|
||||
const productId = item.id ?? ''
|
||||
let existsInResult = false
|
||||
for (let j = 0; j < result.length; j++) {
|
||||
if (result[j].id == productId) {
|
||||
existsInResult = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (existsInResult) {
|
||||
continue
|
||||
}
|
||||
if (!allowRepeat && productId != '' && selectedIds.indexOf(productId) != -1) {
|
||||
continue
|
||||
}
|
||||
result.push(item)
|
||||
if (!allowRepeat && productId != '') {
|
||||
selectedIds.push(productId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectChannelProducts(primary: Array<Product>, secondary: Array<Product>, fallback: Array<Product>, selectedIds: Array<string>, desiredCount: number): Array<Product> {
|
||||
const result: Array<Product> = []
|
||||
appendChannelProducts(primary, result, selectedIds, desiredCount, false)
|
||||
appendChannelProducts(secondary, result, selectedIds, desiredCount, false)
|
||||
appendChannelProducts(fallback, result, selectedIds, desiredCount, false)
|
||||
appendChannelProducts(primary, result, selectedIds, desiredCount, true)
|
||||
appendChannelProducts(secondary, result, selectedIds, desiredCount, true)
|
||||
appendChannelProducts(fallback, result, selectedIds, desiredCount, true)
|
||||
return result
|
||||
}
|
||||
|
||||
function buildChannelFromTemplate(template: MarketingChannel, products: Array<Product>, labelPrefix: string): MarketingChannel {
|
||||
const mappedProducts: Array<ChannelProduct> = []
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
mappedProducts.push(toChannelProduct(products[i], labelPrefix))
|
||||
}
|
||||
return {
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
subtitle: template.subtitle,
|
||||
badge: template.badge,
|
||||
themeColor: template.themeColor,
|
||||
bgColor: template.bgColor,
|
||||
routeType: template.routeType,
|
||||
layoutType: template.layoutType,
|
||||
products: mappedProducts,
|
||||
moreProducts: mappedProducts
|
||||
} as MarketingChannel
|
||||
}
|
||||
|
||||
function logChannelProducts(channelTitle: string, products: Array<Product>): void {
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
const item = products[i]
|
||||
console.log('[home-channel] ' + channelTitle + ' product:', item.id, item.name ?? '', getRealProductImage(item), getRealSalePrice(item), getRealMarketPrice(item))
|
||||
}
|
||||
}
|
||||
|
||||
function buildRealRecommendMarketingChannels(products: Array<Product>): MarketingChannel[] {
|
||||
console.log('[home-channel] buildRealRecommendMarketingChannels input count:', products.length)
|
||||
const templates = getRecommendMarketingChannels()
|
||||
if (products.length == 0 || templates.length == 0) {
|
||||
console.log('[home-channel] fallback to mock channel data')
|
||||
return templates
|
||||
}
|
||||
const uniqueProducts = dedupeProducts(products)
|
||||
if (uniqueProducts.length == 0) {
|
||||
console.log('[home-channel] fallback to mock channel data')
|
||||
return templates
|
||||
}
|
||||
const selectedIds: Array<string> = []
|
||||
const discountCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'discount'), 'discount')
|
||||
const qualityCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'quality'), 'quality')
|
||||
const cheapCandidates = mergeUniqueProductLists(
|
||||
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-9')),
|
||||
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-19')),
|
||||
sortProductsByPriceAsc(uniqueProducts)
|
||||
)
|
||||
const liveCandidates = mergeUniqueProductLists(
|
||||
sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'live'), 'hot'),
|
||||
sortProductsByScoreDesc(discountCandidates, 'discount'),
|
||||
sortProductsByScoreDesc(uniqueProducts, 'hot')
|
||||
)
|
||||
const hotFallback = sortProductsByScoreDesc(uniqueProducts, 'hot')
|
||||
const cheapFallback = sortProductsByPriceAsc(uniqueProducts)
|
||||
|
||||
const subsidyProducts = selectChannelProducts(discountCandidates, hotFallback, hotFallback, selectedIds, 2)
|
||||
const qualityProducts = selectChannelProducts(qualityCandidates, hotFallback, hotFallback, selectedIds, 2)
|
||||
const cheapProducts = selectChannelProducts(cheapCandidates, cheapFallback, hotFallback, selectedIds, 2)
|
||||
const liveProducts = selectChannelProducts(liveCandidates, discountCandidates, hotFallback, selectedIds, 2)
|
||||
|
||||
logChannelProducts('百亿补贴', subsidyProducts)
|
||||
logChannelProducts('品质生活', qualityProducts)
|
||||
logChannelProducts('9.9包邮', cheapProducts)
|
||||
logChannelProducts('直播低价', liveProducts)
|
||||
|
||||
const mappedChannels: Array<MarketingChannel> = []
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
const template = templates[i]
|
||||
if (template.id == 'subsidy') {
|
||||
mappedChannels.push(buildChannelFromTemplate(template, subsidyProducts, '补贴价'))
|
||||
continue
|
||||
}
|
||||
if (template.id == 'quality-life') {
|
||||
mappedChannels.push(buildChannelFromTemplate(template, qualityProducts, '实惠'))
|
||||
continue
|
||||
}
|
||||
if (template.id == 'cheap-mail') {
|
||||
const cheapMappedProducts: Array<ChannelProduct> = []
|
||||
for (let j = 0; j < cheapProducts.length; j++) {
|
||||
const cheapProduct = cheapProducts[j]
|
||||
const label = getRealSalePrice(cheapProduct) <= 9.9 ? '9.9包邮' : '特价'
|
||||
cheapMappedProducts.push(toChannelProduct(cheapProduct, label))
|
||||
}
|
||||
mappedChannels.push({
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
subtitle: template.subtitle,
|
||||
badge: template.badge,
|
||||
themeColor: template.themeColor,
|
||||
bgColor: template.bgColor,
|
||||
routeType: template.routeType,
|
||||
layoutType: template.layoutType,
|
||||
products: cheapMappedProducts,
|
||||
moreProducts: cheapMappedProducts
|
||||
} as MarketingChannel)
|
||||
continue
|
||||
}
|
||||
if (template.id == 'live-low-price') {
|
||||
mappedChannels.push(buildChannelFromTemplate(template, liveProducts, '直播价'))
|
||||
continue
|
||||
}
|
||||
mappedChannels.push(template)
|
||||
}
|
||||
return mappedChannels
|
||||
}
|
||||
|
||||
function buildSimpleCategoryChannels(categoryId: string, products: Array<Product> = []): SimpleCategoryChannel[] {
|
||||
const dedupedProducts = dedupeProducts(products)
|
||||
if (dedupedProducts.length == 0) {
|
||||
return [] as Array<SimpleCategoryChannel>
|
||||
}
|
||||
const hotProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'hot')
|
||||
const qualityProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'quality')
|
||||
const firstChannelCovers: Array<string> = []
|
||||
const secondChannelCovers: Array<string> = []
|
||||
for (let i = 0; i < hotProductsForCategory.length && firstChannelCovers.length < 2; i++) {
|
||||
firstChannelCovers.push(getRealProductImage(hotProductsForCategory[i]))
|
||||
}
|
||||
for (let i = 0; i < qualityProductsForCategory.length && secondChannelCovers.length < 2; i++) {
|
||||
secondChannelCovers.push(getRealProductImage(qualityProductsForCategory[i]))
|
||||
}
|
||||
while (firstChannelCovers.length < 2) {
|
||||
firstChannelCovers.push('/static/images/default.png')
|
||||
}
|
||||
while (secondChannelCovers.length < 2) {
|
||||
secondChannelCovers.push('/static/images/default.png')
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: categoryId + '-rank',
|
||||
title: '热销榜',
|
||||
subtitle: '真实商品热度精选',
|
||||
routeType: 'rank',
|
||||
icon: '热',
|
||||
coverImages: firstChannelCovers,
|
||||
categoryId
|
||||
} as SimpleCategoryChannel,
|
||||
{
|
||||
id: categoryId + '-quality',
|
||||
title: '品质优选',
|
||||
subtitle: '真实好物口碑推荐',
|
||||
routeType: 'quality',
|
||||
icon: '精',
|
||||
coverImages: secondChannelCovers,
|
||||
categoryId
|
||||
} as SimpleCategoryChannel
|
||||
]
|
||||
}
|
||||
|
||||
async function loadCategoryChannelCards(categoryId: string): Promise<void> {
|
||||
try {
|
||||
const channelResult = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, categoryChannelLoadLimit)
|
||||
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId, channelResult.data)
|
||||
} catch (error) {
|
||||
console.error('[home-channel] 加载分类频道卡片失败', categoryId, error)
|
||||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||||
}
|
||||
}
|
||||
|
||||
function buildVisibleRecommendChannels(): MarketingChannel[] {
|
||||
const source = getRecommendMarketingChannels()
|
||||
const visible: MarketingChannel[] = []
|
||||
@@ -937,16 +1239,16 @@ function buildVisibleRecommendChannels(): MarketingChannel[] {
|
||||
visible.push(channel)
|
||||
}
|
||||
return visible
|
||||
}
|
||||
}
|
||||
|
||||
function applyChannelDisplay(categoryId: string): void {
|
||||
if (categoryId === 'recommend') {
|
||||
marketingChannels.value = buildVisibleRecommendChannels()
|
||||
categorySimpleChannels.value = []
|
||||
marketingChannels.value = [] as Array<MarketingChannel>
|
||||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||||
return
|
||||
}
|
||||
marketingChannels.value = []
|
||||
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId)
|
||||
marketingChannels.value = [] as Array<MarketingChannel>
|
||||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||||
}
|
||||
|
||||
function buildChannelDetailUrl(channelId: string, routeType: string, categoryId: string): string {
|
||||
@@ -1440,11 +1742,18 @@ async function loadHotProducts(page: number, limit: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
setHotProducts(products)
|
||||
if (currentFeedCategoryId.value === 'recommend' && page <= 1) {
|
||||
marketingChannels.value = buildRealRecommendMarketingChannels(products)
|
||||
}
|
||||
hasMore.value = result.hasmore
|
||||
currentPage.value = page
|
||||
} catch (error) {
|
||||
console.error('加载热销商品失败:', error)
|
||||
hotProducts.value = []
|
||||
if (currentFeedCategoryId.value === 'recommend') {
|
||||
console.log('[home-channel] fallback to mock channel data')
|
||||
marketingChannels.value = buildVisibleRecommendChannels()
|
||||
}
|
||||
hasMore.value = false
|
||||
}
|
||||
}
|
||||
@@ -1476,14 +1785,18 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
|
||||
await syncCategoryLayout(categoryId)
|
||||
if (categoryId === 'recommend') {
|
||||
try {
|
||||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit)
|
||||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
|
||||
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
|
||||
failedProductImageIds.value = []
|
||||
setHotProducts(result.data)
|
||||
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
|
||||
hasMore.value = result.hasmore
|
||||
currentPage.value = 1
|
||||
} catch (error) {
|
||||
console.error('加载热销商品失败:', error)
|
||||
hotProducts.value = []
|
||||
console.log('[home-channel] fallback to mock channel data')
|
||||
marketingChannels.value = buildVisibleRecommendChannels()
|
||||
hasMore.value = false
|
||||
}
|
||||
} else {
|
||||
@@ -1492,10 +1805,12 @@ async function loadCategoryGoods(categoryId: string): Promise<void> {
|
||||
const result = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, defaultLoadLimit)
|
||||
failedProductImageIds.value = []
|
||||
setHotProducts(result.data)
|
||||
await loadCategoryChannelCards(categoryId)
|
||||
hasMore.value = result.hasmore
|
||||
} catch (e) {
|
||||
console.error('分类商品加载失败', e)
|
||||
hotProducts.value = []
|
||||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -1520,12 +1835,16 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
|
||||
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
|
||||
applyChannelDisplay(item.id)
|
||||
try {
|
||||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, defaultLoadLimit)
|
||||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
|
||||
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
|
||||
setHotProducts(result.data)
|
||||
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
|
||||
hasMore.value = result.hasmore
|
||||
} catch (error) {
|
||||
console.error('加载推荐商品失败:', error)
|
||||
hotProducts.value = []
|
||||
console.log('[home-channel] fallback to mock channel data')
|
||||
marketingChannels.value = buildVisibleRecommendChannels()
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -1545,10 +1864,12 @@ async function refreshHomeCategory(item: CategoryItem): Promise<void> {
|
||||
try {
|
||||
const result = await supabaseService.getMedicalMallProductsByCategory(item.id, 1, defaultLoadLimit)
|
||||
setHotProducts(result.data)
|
||||
await loadCategoryChannelCards(item.id)
|
||||
hasMore.value = result.hasmore
|
||||
} catch (error) {
|
||||
console.error('分类商品加载失败', error)
|
||||
hotProducts.value = []
|
||||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||||
hasMore.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -1857,7 +2178,8 @@ const switchSort = (sortId: string) => {
|
||||
}
|
||||
hasMore.value = true // 重置加载更多状态
|
||||
// 重新加载热销商品,排序由 Supabase 服务处理
|
||||
loadHotProducts(1, defaultLoadLimit)
|
||||
const nextLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
|
||||
loadHotProducts(1, nextLimit)
|
||||
}
|
||||
|
||||
// 切换筛选器
|
||||
@@ -1909,12 +2231,13 @@ const loadMore = async () => {
|
||||
showLoadMore.value = true
|
||||
loading.value = true
|
||||
try {
|
||||
const pageLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
|
||||
const nextPage = currentPage.value + 1
|
||||
const currentCount = hotProducts.value.length
|
||||
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value)
|
||||
|
||||
if (currentFeedCategoryId.value === 'recommend') {
|
||||
const result = await fetchSortedProductsPage(nextPage, defaultLoadLimit)
|
||||
const result = await fetchSortedProductsPage(nextPage, pageLimit)
|
||||
const newProducts = result.data
|
||||
|
||||
if (newProducts.length == 0) {
|
||||
@@ -2025,7 +2348,7 @@ const onScan = (): void => {
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('扫码失败:', err)
|
||||
console.error('扫码失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,29 +195,12 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="recommend-section">
|
||||
<view class="recommend-header">
|
||||
<text class="recommend-title">猜你喜欢</text>
|
||||
<text class="recommend-subtitle">精选推荐,继续逛一逛</text>
|
||||
</view>
|
||||
<view class="recommend-grid">
|
||||
<view v-for="item in recommendProducts" :key="item.id" class="recommend-card" @click="goToRecommendProduct(item)">
|
||||
<image :src="item.image" class="recommend-image" mode="aspectFill" />
|
||||
<view class="recommend-info">
|
||||
<text class="recommend-name">{{ item.name }}</text>
|
||||
<view class="recommend-meta-row">
|
||||
<text class="recommend-price">¥{{ item.price }}</text>
|
||||
<text class="recommend-tag">{{ item.tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="recommend-load-more" @click="loadRecommendProducts(false)">
|
||||
<text v-if="recommendLoading" class="recommend-load-text">正在加载更多...</text>
|
||||
<text v-else-if="!recommendHasMore && recommendInitialized" class="recommend-load-text">没有更多了</text>
|
||||
<text v-else class="recommend-load-text">上拉加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
<GuessYouLike
|
||||
title="猜你喜欢"
|
||||
:pageSize="8"
|
||||
:loadMoreKey="guessLoadMoreKey"
|
||||
@productClick="handleGuessProductClick"
|
||||
/>
|
||||
|
||||
<view class="profile-bottom-safe"></view>
|
||||
</view>
|
||||
@@ -229,6 +212,7 @@
|
||||
import { UserType } from '@/types/mall-types.uts'
|
||||
import supabaseService from '@/utils/supabaseService.uts'
|
||||
import { goToLogin } from '@/utils/utils.uts'
|
||||
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
|
||||
|
||||
type UserStatsType = {
|
||||
points: number
|
||||
@@ -290,6 +274,9 @@ type PendingReceiptGoodsType = {
|
||||
type ModalSuccessResult = { confirm: boolean; cancel: boolean }
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GuessYouLike
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
@@ -345,6 +332,7 @@ export default {
|
||||
recommendBottomLocked: false,
|
||||
recommendViewportHeight: 0,
|
||||
pageWindowHeight: 0,
|
||||
guessLoadMoreKey: 0,
|
||||
statusBarHeight: 0,
|
||||
isAndroidApp: false,
|
||||
capsuleTop: 0,
|
||||
@@ -376,7 +364,6 @@ export default {
|
||||
this.initPage()
|
||||
this.loadUserProfile()
|
||||
this.loadOrders()
|
||||
this.loadRecommendProducts(true)
|
||||
|
||||
// 监听订单更新事件
|
||||
uni.$on('orderUpdated', this.handleOrderUpdated)
|
||||
@@ -474,24 +461,6 @@ export default {
|
||||
return result
|
||||
},
|
||||
|
||||
mockRecommendProducts(page: number, pageSize: number): Array<RecommendProductType> {
|
||||
const list: Array<RecommendProductType> = []
|
||||
if (page >= 5) {
|
||||
return list
|
||||
}
|
||||
for (let i: number = 0; i < pageSize; i++) {
|
||||
const index = (page - 1) * pageSize + i + 1
|
||||
list.push({
|
||||
id: 'mock-recommend-' + index,
|
||||
name: '猜你喜欢 推荐商品 ' + index,
|
||||
image: '/static/images/default.png',
|
||||
price: 9.9 + index,
|
||||
tag: '已售' + (100 + index) + '+'
|
||||
} as RecommendProductType)
|
||||
}
|
||||
return list
|
||||
},
|
||||
|
||||
async fetchRecommendProducts(page: number, pageSize: number): Promise<Array<RecommendProductType>> {
|
||||
console.log('[profile推荐] fetchRecommendProducts 请求 page=', page, 'pageSize=', pageSize)
|
||||
const result = await supabaseService.searchProducts('', page, pageSize, 'sales')
|
||||
@@ -578,14 +547,7 @@ export default {
|
||||
this.recommendInitialized = true
|
||||
} catch (e) {
|
||||
console.error('加载推荐商品失败:', e)
|
||||
|
||||
if (reset && this.recommendProducts.length === 0) {
|
||||
const mockList = this.mockRecommendProducts(1, this.recommendPageSize)
|
||||
this.recommendProducts = mockList
|
||||
this.recommendPage = 2
|
||||
this.recommendHasMore = true
|
||||
this.recommendInitialized = true
|
||||
}
|
||||
this.recommendHasMore = false
|
||||
} finally {
|
||||
this.recommendLoading = false
|
||||
if (!reset && this.recommendPendingLoad && this.recommendHasMore) {
|
||||
@@ -597,55 +559,20 @@ export default {
|
||||
},
|
||||
|
||||
onRecommendScrollToLower() {
|
||||
console.log('[profile推荐] scrolltolower 触发')
|
||||
this.recommendBottomLocked = true
|
||||
this.loadRecommendProducts(false)
|
||||
this.guessLoadMoreKey = this.guessLoadMoreKey + 1
|
||||
},
|
||||
|
||||
onRecommendScroll(event: any) {
|
||||
if (this.recommendLoading || !this.recommendHasMore) {
|
||||
return
|
||||
},
|
||||
|
||||
handleGuessProductClick(productId: string) {
|
||||
if (productId == null || productId === '') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const eventObj = this.toRecommendScrollJson(event)
|
||||
let detailObj: UTSJSONObject | null = null
|
||||
if (eventObj != null) {
|
||||
detailObj = this.toRecommendScrollJson(eventObj.get('detail'))
|
||||
}
|
||||
if (detailObj == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = this.readRecommendScrollMetric(detailObj, 'scrollTop')
|
||||
const scrollHeight = this.readRecommendScrollMetric(detailObj, 'scrollHeight')
|
||||
let clientHeight = this.readRecommendScrollMetric(detailObj, 'clientHeight')
|
||||
|
||||
if (clientHeight <= 0) {
|
||||
clientHeight = this.recommendViewportHeight
|
||||
}
|
||||
|
||||
console.log('[profile推荐] scroll事件 scrollTop=', scrollTop, 'scrollHeight=', scrollHeight, 'clientHeight=', clientHeight)
|
||||
|
||||
if (scrollHeight <= 0 || clientHeight <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const distanceToBottom = scrollHeight - scrollTop - clientHeight
|
||||
if (distanceToBottom > 260) {
|
||||
this.recommendBottomLocked = false
|
||||
}
|
||||
if (distanceToBottom <= 180) {
|
||||
console.log('[profile推荐] scroll 兜底触底 distanceToBottom=', distanceToBottom)
|
||||
if (this.recommendBottomLocked) {
|
||||
this.recommendPendingLoad = true
|
||||
return
|
||||
}
|
||||
this.recommendBottomLocked = true
|
||||
this.loadRecommendProducts(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[profile推荐] 处理推荐滚动失败', e)
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}`
|
||||
})
|
||||
},
|
||||
|
||||
resetGuestProfileState() {
|
||||
@@ -1045,9 +972,6 @@ export default {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == null || userId === '') {
|
||||
this.resetGuestProfileState()
|
||||
if (!this.recommendInitialized) {
|
||||
this.loadRecommendProducts(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1055,9 +979,6 @@ export default {
|
||||
this.loadUserProfile()
|
||||
this.loadOrders()
|
||||
this.updateCouponCount() // 更新优惠券数量
|
||||
if (!this.recommendInitialized) {
|
||||
this.loadRecommendProducts(true)
|
||||
}
|
||||
},
|
||||
|
||||
async updateCouponCount() {
|
||||
@@ -1545,13 +1466,14 @@ export default {
|
||||
},
|
||||
|
||||
payOrder(order: OrderItemType) {
|
||||
const paymentAmount = order.actual_amount
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId == null || userId === '') {
|
||||
goToLogin(`/pages/mall/consumer/payment?orderId=${order.id}`)
|
||||
goToLogin(`/pages/mall/consumer/payment?orderId=${order.id}&amount=${paymentAmount}`)
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/mall/consumer/payment?orderId=${order.id}`
|
||||
url: `/pages/mall/consumer/payment?orderId=${order.id}&amount=${paymentAmount}`
|
||||
})
|
||||
},
|
||||
|
||||
@@ -2693,4 +2615,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user