完善下单逻辑及其ui展示,修复支付倒计时显示错误bug

This commit is contained in:
2026-05-25 15:35:41 +08:00
parent d25f80ccdd
commit cecb51a8e2
40 changed files with 13040 additions and 3217 deletions

View File

@@ -1,8 +1,8 @@
<template>
<template>
<view class="search-page">
<view class="search-navbar" :style="navbarStyle">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-icon"><</text>
</view>
<view class="jd-search-box">
@@ -12,7 +12,7 @@
:value="searchKeyword"
@input="onInput"
@confirm="onSearch"
:placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'"
:placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'"
placeholder-class="jd-placeholder"
:focus="autoFocus"
confirm-type="search"
@@ -23,7 +23,7 @@
</view>
<view class="camera-wrap" @tap.stop="handleCameraSearch">
<text class="camera-icon">📷</text>
<text class="camera-icon"></text>
</view>
<view class="search-submit-btn" @tap.stop="onSearch">
@@ -32,23 +32,25 @@
</view>
</view>
<!-- 错误状态(模拟服务器超时) -->
<!-- 错误状态 -->
<view v-if="isError" class="error-state" @click="retryLoad">
<view class="error-content">
<text class="error-icon">⚠️</text>
<text class="error-title">加载服务超时</text>
<text class="error-icon">!</text>
<text class="error-title">加载服务超时</text>
<text class="error-desc">请点击屏幕重试</text>
</view>
</view>
<!-- 主内容区域:使用 scroll-view 支持安卓端滚动 -->
<!-- 涓诲唴瀹瑰尯鍩燂細浣跨敤 scroll-view 鏀寔瀹夊崜绔粴鍔?-->
<scroll-view
v-else
class="main-content"
direction="vertical"
:show-scrollbar="false"
>
<!-- 初始状态(无搜索词) -->
v-else
class="main-content"
direction="vertical"
:show-scrollbar="false"
:lower-threshold="120"
@scrolltolower="handleMainScrollToLower"
>
<!-- 初始状态 -->
<view v-if="searchKeyword == '' && showResults == false">
<!-- 搜索历史 -->
<view v-if="searchHistory.length > 0" class="search-history">
@@ -97,51 +99,21 @@
>
<text class="hot-rank" :class="index < 3 ? 'top-three' : ''">{{ index + 1 }}</text>
<text class="hot-text">{{ item.keyword }}</text>
<text v-if="item.hot == true" class="hot-icon">🔥</text>
<text v-if="item.hot == true" class="hot-icon"></text>
</view>
</view>
</view>
<!-- 推荐商品(猜你喜欢 -->
<!-- 猜你喜欢 -->
<view class="guess-you-like">
<view class="section-header">
<view class="title-with-icon">
<text class="section-icon">✨</text>
<text class="section-title">猜你喜欢</text>
</view>
<text class="refresh-btn" @click="refreshGuessList">换一批</text>
</view>
<view class="guess-grid">
<view
v-for="item in recommendList"
:key="(item.id != null ? item.id : item.name)"
class="guess-item"
@click="viewProductDetail(item)"
>
<image class="guess-img" :src="item.image" mode="aspectFill" />
<view v-if="item.cardTags.length > 0" class="card-tags-row">
<text v-for="(tag, index) in item.cardTags" :key="item.id + '-guess-tag-' + index" class="card-tag">{{ tag }}</text>
</view>
<text class="guess-name" :lines="2">{{ item.name }}</text>
<text v-if="item.highlight !== ''" class="card-highlight">{{ item.highlight }}</text>
<view v-if="item.serviceTags.length > 0" class="service-tags-row">
<text v-for="(tag, index) in item.serviceTags" :key="item.id + '-guess-service-' + index" class="service-tag">{{ tag }}</text>
</view>
<view class="guess-bottom">
<view class="price-stack">
<text class="guess-price">¥{{ item.price }}</text>
<text v-if="item.marketPrice !== ''" class="market-price">¥{{ item.marketPrice }}</text>
</view>
<view class="guess-add-btn" @click.stop="addToCart(item)">
<text class="guess-add-icon">+</text>
</view>
</view>
<text v-if="item.salesText !== ''" class="card-sales-text">{{ item.salesText }}</text>
</view>
</view>
<GuessYouLike
title="猜你喜欢"
:pageSize="8"
:loadMoreKey="guessLoadMoreKey"
@productClick="goToProductDetail"
/>
</view>
</view>
</view>
<!-- 搜索建议 -->
<view v-if="searchKeyword != '' && showResults == false" class="search-suggestions">
@@ -152,7 +124,7 @@
class="suggestion-item"
@click="selectSuggestion(suggestion)"
>
<view class="suggestion-icon">🔍</view>
<view class="suggestion-icon"></view>
<text class="suggestion-text">{{ suggestion }}</text>
</view>
</view>
@@ -176,7 +148,7 @@
<image class="shop-logo" :src="shop.logo" mode="aspectFill" />
<view class="shop-info">
<text class="shop-name-txt">{{ shop.name }}</text>
<text class="shop-products-txt">共{{ shop.productCount }}件商品</text>
<text class="shop-products-txt">共 {{ shop.productCount }} 件商品</text>
</view>
</view>
</view>
@@ -195,13 +167,13 @@
class="filter-tab"
:class="{ active: activeSort === 'sales' }"
@click="switchSort('sales')"
>销量</text>
>销量</text>
<text
class="filter-tab"
:class="{ active: activeSort === 'price' }"
@click="switchSort('price')"
>
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
</text>
</view>
</view>
@@ -235,14 +207,14 @@
</view>
</view>
<!-- 空结果 - 仅在非加载状态且无结果时显示 -->
<!-- 空结果 -->
<view v-if="!loading && searchResults.length === 0" class="empty-result">
<text class="empty-icon">🤔</text>
<text class="empty-icon">!</text>
<text class="empty-text">未找到相关商品</text>
<text class="empty-sub">换个关键词试试吧</text>
</view>
<!-- 加载更多/加载中 - 在加载状态或有更多数据时显示 -->
<!-- 加载更多 -->
<view v-if="loading" class="loading-more">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
@@ -252,41 +224,14 @@
<text class="no-more-text">--- 到底了 ---</text>
</view>
<view v-if="searchResults.length > 0 && recommendList.length > 0" class="guess-you-like" style="margin-top: 16rpx;">
<view class="section-header">
<view class="title-with-icon">
<text class="section-icon">✨</text>
<text class="section-title">猜你喜欢</text>
</view>
</view>
<view class="guess-grid">
<view
v-for="item in recommendList"
:key="(item.id != null ? item.id : '') + '_rec'"
class="guess-item"
@click="viewProductDetail(item)"
>
<image class="guess-img" :src="item.image" mode="aspectFill" />
<view v-if="item.cardTags.length > 0" class="card-tags-row">
<text v-for="(tag, index) in item.cardTags" :key="item.id + '-rec-tag-' + index" class="card-tag">{{ tag }}</text>
</view>
<text class="guess-name" :lines="2">{{ item.name }}</text>
<text v-if="item.highlight !== ''" class="card-highlight">{{ item.highlight }}</text>
<view v-if="item.serviceTags.length > 0" class="service-tags-row">
<text v-for="(tag, index) in item.serviceTags" :key="item.id + '-rec-service-' + index" class="service-tag">{{ tag }}</text>
</view>
<view class="guess-bottom">
<view class="price-stack">
<text class="guess-price">¥{{ item.price }}</text>
<text v-if="item.marketPrice !== ''" class="market-price">¥{{ item.marketPrice }}</text>
</view>
<view class="guess-add-btn" @click.stop="addToCart(item)">
<text class="guess-add-icon">+</text>
</view>
</view>
<text v-if="item.salesText !== ''" class="card-sales-text">{{ item.salesText }}</text>
</view>
</view>
<view v-if="searchResults.length > 0" class="guess-you-like" style="margin-top: 16rpx;">
<GuessYouLike
title="猜你喜欢"
:pageSize="8"
:excludeProductIds="searchResultProductIds"
:loadMoreKey="guessLoadMoreKey"
@productClick="goToProductDetail"
/>
</view>
</view>
@@ -302,6 +247,7 @@ import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
// 状态定义
const statusBarHeight = ref(0)
@@ -316,6 +262,7 @@ const loading = ref(false)
const hasMore = ref(true)
const isError = ref(false)
const autoFocus = ref(true)
const guessLoadMoreKey = ref<number>(0)
const navbarStyle = computed(() => {
const top = navBarTop.value > 0 ? navBarTop.value : statusBarHeight.value
@@ -330,7 +277,6 @@ const handleCameraSearch = () => {
const activeSort = ref('default') // 当前排序方式: default, sales, price
const priceSortAsc = ref(false) // 价格排序是否为升序
type HotSearchItemType = {
keyword: string
hot: boolean
@@ -436,21 +382,21 @@ const guessList = ref<Array<GuessItemType>>([])
const allGuessItems = ref<Array<GuessItemType>>([])
const DEFAULT_HOT_KEYWORDS: Array<string> = [
'大疆neo2',
'澶х枂neo2',
'iPhone 15 Pro',
'Nike Air Max 270',
'厨具',
'老干妈',
'钢化膜',
'手机壳',
'零食坚果',
'新鲜水果',
'液态硅胶壳',
'充电宝',
'蓝牙耳机'
'厨具',
'老干妈',
'钢化膜',
'手机壳',
'零食坚果',
'新鲜水果',
'液态硅胶壳',
'充电宝',
'蓝牙耳机'
]
// 推荐商品区(用于 “猜你喜欢/推荐商品”)
// 推荐商品区
const recommendList = ref<Array<GuessItemType>>([])
const recommendPool = ref<Array<GuessItemType>>([])
const recommendPage = ref(0)
@@ -459,6 +405,16 @@ const recommendAppendSize = ref(20)
const loadingRecommend = ref(false)
const searchResults = ref<Array<SearchResultType>>([])
const searchShopResults = ref<Array<ShopResultType>>([])
const searchResultProductIds = computed((): Array<string> => {
const ids: Array<string> = []
for (let i = 0; i < searchResults.value.length; i++) {
const id = searchResults.value[i].id
if (id !== '' && ids.indexOf(id) < 0) {
ids.push(id)
}
}
return ids
})
const SEARCH_HISTORY_KEY = 'consumer_search_history'
@@ -504,8 +460,8 @@ const MAX_EXPANDED_COUNT = 24
const clearHistory = () => {
uni.showModal({
title: '提示',
content: '确定清空搜索历史吗?',
title: '提示',
content: '确定清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
searchHistory.value = []
@@ -525,8 +481,6 @@ const deleteHistoryItem = (index: number) => {
const toggleHistoryEdit = () => { isEditMode.value = !isEditMode.value }
const toggleHistoryExpanded = () => { historyExpanded.value = !historyExpanded.value }
import { computed } from 'vue'
const visibleHistory = computed(() => {
if (historyExpanded.value) {
const maxLen = searchHistory.value.length < MAX_EXPANDED_COUNT ? searchHistory.value.length : MAX_EXPANDED_COUNT
@@ -577,7 +531,7 @@ const loadData = async (): Promise<void> => {
const hotResult = await supabaseService.getHotProducts(30)
hotProducts = hotResult as Product[]
} catch (hotError) {
console.error('获取热销商品失败,使用空列表:', hotError)
console.error('获取热销商品失败,使用空列表:', hotError)
hotProducts = []
}
@@ -656,7 +610,7 @@ const loadData = async (): Promise<void> => {
}
const retryLoad = () => {
uni.showLoading({ title: '重新加载中' })
uni.showLoading({ title: '重新加载中...' })
setTimeout(() => {
uni.hideLoading()
loadData()
@@ -774,7 +728,7 @@ const performSearch = async (): Promise<void> => {
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0,
marketPrice: formatMarketPriceText(p),
specification: p.specification ?? '标准规格',
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count ?? 0,
salesText: formatSalesText(p),
@@ -848,7 +802,7 @@ const initPage = () => {
}
}
} catch (e) {
console.error('初始化失败', e)
console.error('初始化失败:', e)
isError.value = true
}
}
@@ -891,16 +845,16 @@ const onSearch = () => {
}
if (effective === '') return
addToHistory(effective)
// 如果搜索词来自 placeholder保输入框为空但执行搜索
// 如果搜索词来自 placeholder输入框为空但执行搜索
if (userInput === '') {
// 保持 searchKeyword 为空 but perform search with effective
// 保持 searchKeyword 为空,但使用 effective 搜索
searchKeyword.value = ''
}
// 将 searchKeyword 临时设置为 effective 以便 performSearch 使用performSearch 使用 searchKeyword
// 临时将 searchKeyword 设置为 effective,供 performSearch 使用
const prev = searchKeyword.value
searchKeyword.value = effective
performSearch()
// 恢复输入框为空状态(如果用户未输入)
// 如果用户没有手动输入,则恢复为空
if (userInput === '') searchKeyword.value = prev
}
@@ -934,12 +888,12 @@ const switchSort = (type: string) => {
priceSortAsc.value = !priceSortAsc.value
} else {
activeSort.value = 'price'
priceSortAsc.value = true // 默认升序
priceSortAsc.value = true // 默认升序
}
} else {
activeSort.value = type
}
// 重新执行搜索以获取正确排序的数据
// 重新搜索以获取正确排序的数据
performSearch()
}
@@ -984,7 +938,7 @@ const loadMore = async (): Promise<void> => {
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0,
marketPrice: formatMarketPriceText(p),
specification: p.specification ?? '标准规格',
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count ?? 0,
salesText: formatSalesText(p),
@@ -1004,67 +958,26 @@ const loadMore = async (): Promise<void> => {
}
}
async function loadRecommendGoods(appendSize: number): Promise<void> {
if (loadingRecommend.value) return
loadingRecommend.value = true
try {
// 如果后端支持分页接口,可在此调用;当前使用本地 pool
if (appendSize == null || appendSize <= 0) {
// 不追加时确保至少保持初始量
recommendList.value = recommendPool.value.slice(0, recommendInitialSize.value)
recommendPage.value = 1
} else {
const startIndex = recommendList.value.length
const endIndex = startIndex + appendSize
const slice = recommendPool.value.slice(startIndex, endIndex)
// 如果池不够,尝试循环或留空
if (slice.length === 0) {
// 将池随机打乱并继续追加(循环播放)
const arr = recommendPool.value.slice()
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const t = arr[i]; arr[i] = arr[j]; arr[j] = t
}
recommendPool.value = arr
const more = recommendPool.value.slice(0, appendSize)
recommendList.value = recommendList.value.concat(more)
} else {
recommendList.value = recommendList.value.concat(slice)
}
recommendPage.value += 1
}
} catch (e) {
console.error('加载推荐商品失败', e)
} finally {
loadingRecommend.value = false
}
}
function refreshGuessList(): void {
loadRecommendGoods(recommendAppendSize.value)
}
function handleReachBottom(): void {
if (showResults.value === true) {
loadMore()
if (hasMore.value) {
loadMore()
return
}
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
return
}
loadRecommendGoods(recommendAppendSize.value)
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
}
function handleMainScrollToLower(): void {
handleReachBottom()
}
onReachBottom(() => {
handleReachBottom()
})
const viewProductDetail = (item: SearchResultType | GuessItemType) => {
const id = (item as GuessItemType).id
const price = (item as GuessItemType).price
const name = (item as GuessItemType).name
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${id}&price=${price}&name=${encodeURIComponent(name)}`
})
}
const viewShopDetail = (shop: ShopResultType) => {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?id=${shop.id}`
@@ -1077,40 +990,40 @@ const addToCart = async (product: SearchResultType | GuessItemType) => {
goToLogin('/pages/mall/consumer/search')
return
}
uni.showLoading({ title: '检查商品...' })
uni.showLoading({ title: '检查商品...' })
try {
// 统一转换为 UTSJSONObject 访问属性
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
// 统一转换为 UTSJSONObject 访问属性
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
const productId = prodObj.getString('id') ?? ''
const merchantId = prodObj.getString('merchant_id') ?? ''
// 检查商品是否有SKU
// 检查商品是否有 SKU
const skus = await supabaseService.getProductSkus(productId)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情选择规格
uni.showToast({ title: '请选择规格', icon: 'none' })
// 有规格时跳商品详情选择规格
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + productId
})
}, 500)
} else {
// 无规格直接加入购物车
uni.showLoading({ title: '添加中...' })
// 无规格直接加入购物车
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
}
}
} catch (e) {
console.error('添加到购物车异常', e)
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ title: '操作异常', icon: 'none' })
uni.showToast({ title: '操作异常', icon: 'none' })
}
}
@@ -1119,31 +1032,31 @@ const openCamera = () => {
count: 1,
sourceType: ['camera'],
success: (res) => {
console.log('拍摄图片路径', (res.tempFilePaths as string[])[0])
uni.showToast({ title: '已启用相机', icon: 'none' })
console.log('拍摄图片路径:', (res.tempFilePaths as string[])[0])
uni.showToast({ title: '已启用相机', icon: 'none' })
},
fail: (err) => {
console.error('启用相机失败', err)
console.error('鍚敤鐩告満澶辫触', err)
}
})
}
const goBack = () => {
if (showResults.value) {
// 如果在搜索结果页,先返回搜索初始页
showResults.value = false
searchKeyword.value = ''
} else {
// 如果在搜索初始页,则返回上一页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
// 如果只有一页(由于深链接或重定向),返回首页
uni.switchTab({
url: '/pages/main/index'
})
}
if (showResults.value) {
// 如果在搜索结果页,先返回搜索初始页
showResults.value = false
searchKeyword.value = ''
} else {
// 如果在搜索初始页,则返回上一页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
// 如果只有一个页面,则回首页
uni.switchTab({
url: '/pages/main/index'
})
}
}
}
</script>
@@ -1158,7 +1071,7 @@ const goBack = () => {
min-height: 100%;
}
/* 店铺搜索结果 */
/* 搴楅摵鎼滅储缁撴灉 */
.shop-results-section {
background-color: #fff;
margin-bottom: 10px;
@@ -1486,7 +1399,7 @@ const goBack = () => {
margin-left: 6rpx;
}
/* 猜你需要 */
/* 鐚滀綘闇€瑕?*/
.guess-you-like {
margin-bottom: 24rpx;
background-color: #ffffff;
@@ -1840,7 +1753,7 @@ const goBack = () => {
font-weight: bold;
}
/* 错误状态 */
/* 閿欒鐘舵€?*/
.error-state {
flex: 1;
display: flex;
@@ -1872,7 +1785,7 @@ const goBack = () => {
color: #999;
}
/* 加载更多 */
/* 鍔犺浇鏇村 */
.loading-more {
padding: 20px 0;
display: flex;
@@ -1932,3 +1845,5 @@ const goBack = () => {
}
</style>