完善下单逻辑及其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

@@ -0,0 +1,363 @@
<template>
<view class="guess-you-like-section">
<view v-if="showTitle" class="guess-header">
<text class="guess-title">{{ titleText }}</text>
</view>
<view v-if="products.length > 0" class="guess-grid">
<view
v-for="product in products"
:key="product.id"
class="guess-card"
@click="handleProductClick(product.id)"
>
<image class="guess-image" :src="product.image" mode="aspectFill" />
<view class="guess-body">
<text class="guess-name" :lines="2">{{ product.name }}</text>
<view class="guess-price-row">
<text class="guess-price">¥{{ product.price.toFixed(2) }}</text>
<text v-if="product.originalPrice > product.price" class="guess-original-price">¥{{ product.originalPrice.toFixed(2) }}</text>
</view>
<text v-if="product.salesText !== ''" class="guess-sales">{{ product.salesText }}</text>
</view>
</view>
</view>
<view v-else-if="!loading && errorText === ''" class="guess-empty">
<text class="guess-empty-text">暂无推荐商品</text>
</view>
<view v-if="loading" class="guess-status-row">
<text class="guess-status-text">加载中...</text>
</view>
<view v-else-if="errorText !== ''" class="guess-status-row">
<text class="guess-status-text">{{ errorText }}</text>
</view>
<view v-else-if="finished && products.length > 0" class="guess-status-row">
<text class="guess-status-text">没有更多了</text>
</view>
</view>
</template>
<script setup lang="uts">
import { computed, onMounted, ref, watch } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type RecommendProduct = {
id: string
name: string
image: string
price: number
originalPrice: number
salesText: string
categoryId: string
}
const DEFAULT_IMAGE = '/static/logo.png'
const props = defineProps({
title: {
type: String,
default: '猜你喜欢'
},
pageSize: {
type: Number,
default: 8
},
categoryId: {
type: String,
default: ''
},
excludeProductIds: {
type: Array,
default: () => []
},
loadMoreKey: {
type: Number,
default: 0
},
source: {
type: String,
default: ''
},
showTitle: {
type: Boolean,
default: true
}
})
const emit = defineEmits<{
(e: 'productClick', productId: string): void
}>()
const products = ref<Array<RecommendProduct>>([])
const page = ref<number>(1)
const loading = ref<boolean>(false)
const finished = ref<boolean>(false)
const errorText = ref<string>('')
const titleText = computed((): string => {
return props.title as string
})
const toExcludeProductIds = (): Array<string> => {
const result: Array<string> = []
try {
const normalized = JSON.parse(JSON.stringify(props.excludeProductIds))
if (Array.isArray(normalized)) {
for (let i = 0; i < normalized.length; i++) {
const item = normalized[i]
if (item != null) {
const itemText = '' + item
if (itemText !== '' && result.indexOf(itemText) < 0) {
result.push(itemText)
}
}
}
}
} catch (e) {
}
return result
}
const getNumberField = (obj: UTSJSONObject, key: string): number => {
try {
const value = obj.getNumber(key)
if (value != null) {
return value
}
} catch (e) {
}
return 0
}
const getStringField = (obj: UTSJSONObject, key: string): string => {
try {
const value = obj.getString(key)
if (value != null) {
return value
}
} catch (e) {
}
return ''
}
const formatSalesText = (salesCount: number): string => {
if (salesCount <= 0) {
return ''
}
if (salesCount >= 10000) {
const value = Math.floor(salesCount / 100) / 100
return '已售 ' + value + '万'
}
return '已售 ' + salesCount
}
const parseProducts = (rows: Array<UTSJSONObject>): Array<RecommendProduct> => {
const result: Array<RecommendProduct> = []
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
const productId = getStringField(row, 'id')
if (productId === '') {
continue
}
const price = getNumberField(row, 'base_price') > 0 ? getNumberField(row, 'base_price') : getNumberField(row, 'price')
const originalPrice = getNumberField(row, 'market_price') > 0 ? getNumberField(row, 'market_price') : getNumberField(row, 'original_price')
const image = getStringField(row, 'main_image_url') !== ''
? getStringField(row, 'main_image_url')
: (getStringField(row, 'image_url') !== '' ? getStringField(row, 'image_url') : DEFAULT_IMAGE)
const salesCount = getNumberField(row, 'sale_count')
const name = getStringField(row, 'name') !== '' ? getStringField(row, 'name') : '精选商品'
result.push({
id: productId,
name,
image,
price: price > 0 ? price : 0,
originalPrice: originalPrice > 0 ? originalPrice : 0,
salesText: formatSalesText(salesCount),
categoryId: getStringField(row, 'category_id')
})
}
return result
}
const loadProducts = async (reset: boolean): Promise<void> => {
if (loading.value) {
return
}
if (!reset && finished.value) {
return
}
if (reset) {
products.value = []
page.value = 1
finished.value = false
errorText.value = ''
}
loading.value = true
try {
const requestParams = new UTSJSONObject()
requestParams.set('limit', props.pageSize as number)
requestParams.set('offset', (page.value - 1) * (props.pageSize as number))
requestParams.set('categoryId', props.categoryId as string)
requestParams.set('source', props.source as string)
requestParams.set('excludeProductIds', toExcludeProductIds())
const rows = await supabaseService.getRecommendProducts(requestParams)
const parsed = parseProducts(rows)
if (reset) {
products.value = parsed
} else {
const merged = products.value.slice()
for (let i = 0; i < parsed.length; i++) {
let exists = false
for (let j = 0; j < merged.length; j++) {
if (merged[j].id === parsed[i].id) {
exists = true
break
}
}
if (!exists) {
merged.push(parsed[i])
}
}
products.value = merged
}
if (parsed.length < (props.pageSize as number)) {
finished.value = true
} else {
page.value = page.value + 1
}
if (parsed.length === 0 && products.value.length === 0) {
finished.value = true
}
} catch (error) {
console.error('加载猜你喜欢失败:', error)
errorText.value = '推荐商品加载失败'
} finally {
loading.value = false
}
}
const handleProductClick = (productId: string): void => {
emit('productClick', productId)
}
watch(() => props.loadMoreKey, (newValue: number, oldValue: number) => {
if (newValue !== oldValue) {
loadProducts(false)
}
})
watch(() => props.categoryId, (newValue: string, oldValue: string) => {
if (newValue !== oldValue) {
loadProducts(true)
}
})
watch(() => JSON.stringify(props.excludeProductIds), (newValue: string, oldValue: string) => {
if (newValue !== oldValue) {
loadProducts(true)
}
})
onMounted(() => {
loadProducts(true)
})
</script>
<style scoped>
.guess-you-like-section {
display: flex;
flex-direction: column;
margin-top: 12px;
}
.guess-header {
padding: 0 4px 10px;
}
.guess-title {
font-size: 16px;
font-weight: bold;
color: #222222;
}
.guess-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.guess-card {
width: 48.5%;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
margin-bottom: 12px;
}
.guess-image {
width: 100%;
height: 148px;
background-color: #f2f2f2;
}
.guess-body {
padding: 10px;
display: flex;
flex-direction: column;
}
.guess-name {
font-size: 13px;
line-height: 19px;
color: #222222;
height: 38px;
margin-bottom: 8px;
}
.guess-price-row {
display: flex;
flex-direction: row;
align-items: baseline;
flex-wrap: wrap;
}
.guess-price {
font-size: 16px;
font-weight: bold;
color: #ff4d4f;
margin-right: 6px;
}
.guess-original-price {
font-size: 11px;
color: #999999;
text-decoration: line-through;
}
.guess-sales {
font-size: 11px;
color: #999999;
margin-top: 6px;
}
.guess-empty,
.guess-status-row {
padding: 14px 0;
display: flex;
justify-content: center;
align-items: center;
}
.guess-empty-text,
.guess-status-text {
font-size: 12px;
color: #999999;
}
</style>