364 lines
7.9 KiB
Plaintext
364 lines
7.9 KiB
Plaintext
<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>
|