完善居家服务商品详情页

This commit is contained in:
2026-05-19 23:10:27 +08:00
parent b5ab00ab12
commit b8b0b453e0
5 changed files with 2870 additions and 358 deletions

View File

@@ -1,18 +1,33 @@
<template>
<view class="hmall-content">
<view v-if="currentCategory != 'recommend' && secondaryCategoryDisplay.length > 0" class="hmall-secondary-panel">
<view
v-for="(item, index) in secondaryCategoryDisplay"
:key="item.id + '-' + index"
:class="['hmall-secondary-item', selectedSubCategoryId == item.id ? 'hmall-secondary-item-active' : '']"
@click="emit('secondary-category-click', item)"
<view v-if="currentCategory != 'recommend' && secondaryCategoryPages.length > 0" class="hmall-secondary-shell">
<scroll-view
class="hmall-secondary-scroll"
direction="horizontal"
:show-scrollbar="false"
:scroll-with-animation="true"
>
<view class="hmall-secondary-icon-wrap">
<image v-if="isImageIcon(item.icon)" class="hmall-secondary-image" :src="item.icon" mode="aspectFill" />
<text v-else class="hmall-secondary-icon-text">{{ getCategoryDisplayIcon(item) }}</text>
<view class="hmall-secondary-pages">
<view
v-for="(page, pageIndex) in secondaryCategoryPages"
:key="page.id + '-' + pageIndex"
class="hmall-secondary-page"
>
<view
v-for="(item, itemIndex) in page.items"
:key="item.id + '-' + itemIndex"
:class="['hmall-secondary-item', selectedSubCategoryId == item.id ? 'hmall-secondary-item-active' : '']"
@click="emit('secondary-category-click', item)"
>
<view class="hmall-secondary-icon-wrap">
<image v-if="item.icon != null && isImageIcon(item.icon)" class="hmall-secondary-image" :src="item.icon" mode="aspectFit" />
<text v-else class="hmall-secondary-icon-text">{{ getCategoryDisplayIcon(item) }}</text>
</view>
<text class="hmall-secondary-name">{{ item.name }}</text>
</view>
</view>
</view>
<text class="hmall-secondary-name">{{ item.name }}</text>
</view>
</scroll-view>
</view>
<view v-if="currentCategory == 'recommend' && marketingChannels.length > 0" class="hmall-recommend-section">
@@ -73,77 +88,86 @@
</view>
</view>
<view class="hmall-feed-divider"></view>
<view class="hmall-products-section">
<view class="hmall-feed-divider"></view>
<view v-if="hotProducts.length > 0" class="hmall-products-grid hmall-products-grid-dense">
<view
v-for="(product, index) in hotProducts"
:key="product.id + '-' + index"
class="hmall-product-card hmall-product-card-skeleton"
@click="emit('select-product', product)"
>
<view class="hmall-product-image-wrapper hmall-product-image-wrapper-fixed">
<image
class="hmall-product-image"
:src="getProductCover(product)"
@error="() => handleProductImageError(product.id)"
mode="aspectFill"
/>
</view>
<view class="hmall-product-body">
<view v-if="getProductCardTags(product).length > 0" class="hmall-product-card-tags">
<text
v-for="(tag, tagIndex) in getProductCardTags(product)"
:key="product.id + '-card-tag-' + tagIndex"
class="hmall-product-card-tag"
>
{{ tag }}
</text>
<view v-if="hotProducts.length > 0" class="hmall-products-grid hmall-products-grid-dense">
<view
v-for="(product, index) in hotProducts"
:key="product.id + '-' + index"
class="hmall-product-card hmall-product-card-skeleton"
@click="emit('select-product', product)"
>
<view class="hmall-product-image-wrapper hmall-product-image-wrapper-fixed">
<image
class="hmall-product-image"
:src="getProductCover(product)"
@error="() => handleProductImageError(product.id)"
mode="aspectFill"
/>
</view>
<text class="hmall-product-title">{{ getProductTitle(product) }}</text>
<text v-if="getProductHighlight(product) != ''" class="hmall-product-highlight">{{ getProductHighlight(product) }}</text>
<view v-if="getProductServiceTags(product).length > 0" class="hmall-product-service-tags">
<text
v-for="(tag, tagIndex) in getProductServiceTags(product)"
:key="product.id + '-service-tag-' + tagIndex"
class="hmall-product-service-tag"
>
{{ tag }}
</text>
<view class="hmall-product-body">
<view v-if="getProductCardTags(product).length > 0" class="hmall-product-card-tags">
<text
v-for="(tag, tagIndex) in getProductCardTags(product)"
:key="product.id + '-card-tag-' + tagIndex"
class="hmall-product-card-tag"
>
{{ tag }}
</text>
</view>
<text class="hmall-product-title">{{ getProductTitle(product) }}</text>
<text v-if="getProductHighlight(product) != ''" class="hmall-product-highlight">{{ getProductHighlight(product) }}</text>
<view v-if="getProductServiceTags(product).length > 0" class="hmall-product-service-tags">
<text
v-for="(tag, tagIndex) in getProductServiceTags(product)"
:key="product.id + '-service-tag-' + tagIndex"
class="hmall-product-service-tag"
>
{{ tag }}
</text>
</view>
<view class="hmall-product-price-row">
<text class="hmall-product-price-value">¥{{ formatProductPrice(product) }}</text>
<text v-if="showMarketPrice(product)" class="hmall-product-market-price">¥{{ formatMarketPrice(product) }}</text>
</view>
<text v-if="getProductSalesText(product) != ''" class="hmall-product-sales">{{ getProductSalesText(product) }}</text>
</view>
<view class="hmall-product-price-row">
<text class="hmall-product-price-value">¥{{ formatProductPrice(product) }}</text>
<text v-if="showMarketPrice(product)" class="hmall-product-market-price">¥{{ formatMarketPrice(product) }}</text>
</view>
<text v-if="getProductSalesText(product) != ''" class="hmall-product-sales">{{ getProductSalesText(product) }}</text>
</view>
</view>
</view>
<view v-else-if="loading" class="hmall-loading-state">
<text class="hmall-loading-text">正在加载商品...</text>
</view>
<view v-else-if="loading" class="hmall-loading-state">
<text class="hmall-loading-text">正在加载商品...</text>
</view>
<EmptyState v-else text="当前分类暂无商品"></EmptyState>
<view v-else class="hmall-empty-wrap">
<EmptyState text="当前分类暂无商品"></EmptyState>
</view>
<view v-if="loading || showLoadMore" class="hmall-load-more-status">
<text class="hmall-loading-text">正在加载更多商品...</text>
</view>
<view v-if="loading || showLoadMore" class="hmall-load-more-status">
<text class="hmall-loading-text">正在加载更多商品...</text>
</view>
<view v-if="!hasMore && hotProducts.length > 0" class="hmall-feed-end-state">
<text class="hmall-feed-end-text">已经到底了</text>
<view v-if="!hasMore && hotProducts.length > 0" class="hmall-feed-end-state">
<text class="hmall-feed-end-text">已经到底了</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import EmptyState from '@/components/consumer/EmptyState.uvue'
import type { Category, Product } from '@/utils/supabaseService.uts'
import type { MarketingChannel, ChannelProduct, SimpleCategoryChannel } from '@/utils/mockChannelData.uts'
const failedProductImageIds = ref<string[]>([])
type SecondaryCategoryPage = {
id: string
items: Array<Category>
}
const props = defineProps({
currentCategory: {
type: String,
@@ -190,6 +214,28 @@ const emit = defineEmits<{
(e: 'select-product', product: Product): void
}>()
const secondaryCategoryPages = computed((): Array<SecondaryCategoryPage> => {
const pages: Array<SecondaryCategoryPage> = []
const source = props.secondaryCategoryDisplay
const pageSize = 10
let pageIndex = 0
while (pageIndex * pageSize < source.length) {
const startIndex = pageIndex * pageSize
const pageItems: Array<Category> = []
for (let i = startIndex; i < source.length && i < startIndex + pageSize; i++) {
pageItems.push(source[i])
}
pages.push({
id: 'secondary-page-' + pageIndex.toString(),
items: pageItems
} as SecondaryCategoryPage)
pageIndex = pageIndex + 1
}
return pages
})
function isImageIcon(icon: string): boolean {
if (icon == '') {
return false
@@ -315,57 +361,83 @@ function showMarketPrice(product: Product): boolean {
<style scoped>
.hmall-content {
padding: 0 20rpx 0;
padding: 0 16rpx 0;
box-sizing: border-box;
background: #f5f5f5;
}
.hmall-secondary-panel {
.hmall-secondary-shell {
margin-top: 16rpx;
margin-bottom: 14rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 18rpx 12rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12rpx;
border-radius: 18rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.hmall-secondary-scroll {
width: 100%;
height: 176rpx;
}
.hmall-secondary-pages {
display: flex;
flex-direction: row;
height: 176rpx;
}
.hmall-secondary-page {
width: 718rpx;
height: 176rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-left: 12rpx;
padding-right: 12rpx;
padding-top: 12rpx;
padding-bottom: 8rpx;
box-sizing: border-box;
flex-shrink: 0;
}
.hmall-secondary-item {
width: 124rpx;
width: 20%;
height: 78rpx;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 8rpx;
border-radius: 16rpx;
background: #f7f7f7;
gap: 10rpx;
justify-content: center;
padding: 0 4rpx;
box-sizing: border-box;
}
.hmall-secondary-item-active {
background: #fff2f2;
}
.hmall-secondary-icon-wrap {
width: 68rpx;
height: 68rpx;
border-radius: 34rpx;
background: #ffffff;
width: 48rpx;
height: 48rpx;
border-radius: 16rpx;
background: #f6f6f6;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-bottom: 6rpx;
overflow: hidden;
}
.hmall-secondary-image {
width: 68rpx;
height: 68rpx;
border-radius: 34rpx;
width: 48rpx;
height: 48rpx;
border-radius: 16rpx;
}
.hmall-secondary-icon-text {
font-size: 28rpx;
color: #e2231a;
font-weight: 700;
font-size: 26rpx;
line-height: 1;
}
.hmall-secondary-name {
@@ -379,6 +451,11 @@ function showMarketPrice(product: Product): boolean {
white-space: nowrap;
}
.hmall-secondary-item-active .hmall-secondary-name {
color: #e1251b;
font-weight: 700;
}
.hmall-recommend-section {
display: flex;
flex-direction: row;
@@ -606,11 +683,17 @@ function showMarketPrice(product: Product): boolean {
margin-bottom: 12rpx;
}
.hmall-products-section {
background: #f5f5f5;
padding-bottom: 8rpx;
}
.hmall-products-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
background: #f5f5f5;
margin-top: 0;
min-height: 0;
padding-bottom: 20rpx;
@@ -634,9 +717,9 @@ function showMarketPrice(product: Product): boolean {
width: 100%;
padding-bottom: 100%;
position: relative;
border-radius: 8px;
border-radius: 18rpx 18rpx 0 0;
overflow: hidden;
background: #f5f5f5;
background: #f2f2f2;
}
.hmall-product-image {
@@ -759,6 +842,12 @@ function showMarketPrice(product: Product): boolean {
justify-content: center;
}
.hmall-empty-wrap {
background: #ffffff;
border-radius: 18rpx;
overflow: hidden;
}
.hmall-loading-text,
.hmall-feed-end-text {
font-size: 24rpx;