完善下单逻辑及其ui展示,修复支付倒计时显示错误bug
This commit is contained in:
@@ -52,13 +52,13 @@
|
||||
:key="product.id + '-' + pIndex"
|
||||
class="hmall-recommend-product"
|
||||
>
|
||||
<image class="hmall-recommend-product-image" :src="getChannelProductImage(product)" mode="aspectFill" />
|
||||
<text class="hmall-recommend-product-name">{{ product.shortName }}</text>
|
||||
<image class="hmall-recommend-product-image" :src="getChannelProductImage(product)" mode="aspectFill" @error="handleChannelProductImageError(product.id)" />
|
||||
<text class="hmall-recommend-product-name">{{ getChannelProductTitle(product) }}</text>
|
||||
<view class="hmall-recommend-price-row">
|
||||
<text class="hmall-recommend-product-tag" :style="{ color: channel.themeColor }">{{ product.tag }}</text>
|
||||
<text class="hmall-recommend-product-price" :style="{ color: channel.themeColor }">¥{{ formatChannelPrice(product.price) }}</text>
|
||||
<text class="hmall-recommend-product-tag" :style="{ color: channel.themeColor }">{{ getChannelProductTag(product) }}</text>
|
||||
<text class="hmall-recommend-product-price" :style="{ color: channel.themeColor }">¥{{ formatChannelPrice(getChannelSalePrice(product)) }}</text>
|
||||
</view>
|
||||
<text v-if="product.marketPrice > product.price" class="hmall-recommend-market-price">¥{{ formatChannelPrice(product.marketPrice) }}</text>
|
||||
<text v-if="showChannelMarketPrice(product)" class="hmall-recommend-market-price">¥{{ formatChannelPrice(getChannelMarketPrice(product)) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -167,6 +167,7 @@ import type { Category, Product } from '@/utils/supabaseService.uts'
|
||||
import type { MarketingChannel, ChannelProduct, SimpleCategoryChannel } from '@/utils/mockChannelData.uts'
|
||||
|
||||
const failedProductImageIds = ref<string[]>([])
|
||||
const failedChannelImageIds = ref<string[]>([])
|
||||
|
||||
type SecondaryCategoryPage = {
|
||||
id: string
|
||||
@@ -266,9 +267,50 @@ function getCategoryDisplayIcon(category: Category): string {
|
||||
}
|
||||
|
||||
function getChannelProductImage(product: ChannelProduct): string {
|
||||
if (failedChannelImageIds.value.indexOf(product.id) != -1) {
|
||||
return '/static/images/default.png'
|
||||
}
|
||||
return product.image != '' ? product.image : '/static/images/default.png'
|
||||
}
|
||||
|
||||
function handleChannelProductImageError(productId: string): void {
|
||||
if (productId == '') {
|
||||
return
|
||||
}
|
||||
if (failedChannelImageIds.value.indexOf(productId) == -1) {
|
||||
failedChannelImageIds.value.push(productId)
|
||||
}
|
||||
}
|
||||
|
||||
function getChannelProductTitle(product: ChannelProduct): string {
|
||||
if (product.shortName != null && product.shortName != '') {
|
||||
return product.shortName
|
||||
}
|
||||
if (product.name != null && product.name != '') {
|
||||
return product.name
|
||||
}
|
||||
return '商品补充中'
|
||||
}
|
||||
|
||||
function getChannelProductTag(product: ChannelProduct): string {
|
||||
if (product.tag != null && product.tag != '') {
|
||||
return product.tag
|
||||
}
|
||||
return '活动价'
|
||||
}
|
||||
|
||||
function getChannelSalePrice(product: ChannelProduct): number {
|
||||
return product.price > 0 ? product.price : 0
|
||||
}
|
||||
|
||||
function getChannelMarketPrice(product: ChannelProduct): number {
|
||||
return product.marketPrice > 0 ? product.marketPrice : 0
|
||||
}
|
||||
|
||||
function showChannelMarketPrice(product: ChannelProduct): boolean {
|
||||
return getChannelMarketPrice(product) > getChannelSalePrice(product)
|
||||
}
|
||||
|
||||
function formatChannelPrice(price: number): string {
|
||||
const rounded = Math.round(price)
|
||||
if (Math.abs(price - rounded) < 0.001) {
|
||||
|
||||
363
components/mall/GuessYouLike/GuessYouLike.uvue
Normal file
363
components/mall/GuessYouLike/GuessYouLike.uvue
Normal 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>
|
||||
Reference in New Issue
Block a user