鸿蒙OSUniApp智能商品展示实战:打造高性能的动态排序系统#三方框架 #Uniapp
UniApp智能商品展示实战:打造高性能的动态排序系统
引言
在电商应用开发中,商品展示和智能排序是提升用户体验的关键因素。随着HarmonyOS生态的发展,用户对应用的性能和交互体验要求越来越高。本文将深入探讨如何在UniApp中实现一个性能优异、体验流畅的智能商品排序展示系统。
技术方案设计
1. 核心功能规划
-
多维度排序支持
- 价格排序(升序/降序)
- 销量排序
- 好评率排序
- 综合排序算法
- 智能推荐排序
-
性能优化重点
- 虚拟列表实现
- 数据分页加载
- 排序状态缓存
- 图片懒加载
2. 技术选型
- 前端框架:UniApp + Vue3 + TypeScript
- 状态管理:Pinia
- 数据处理:Lodash-es
- 虚拟列表:vue-virtual-scroller
- 图片优化:自定义懒加载指令
核心代码实现
1. 商品列表组件
<!-- components/ProductList.vue -->
<template><view class="product-list"><!-- 排序工具栏 --><view class="sort-toolbar"><view v-for="(item, index) in sortOptions" :key="index"class="sort-item":class="{ active: currentSort === item.value }"@tap="handleSort(item.value)"><text>{{ item.label }}</text><view class="sort-icon" v-if="item.sortable"><text class="iconfont icon-arrow-up":class="{ active: currentSort === item.value && sortOrder === 'asc' }"></text><text class="iconfont icon-arrow-down":class="{ active: currentSort === item.value && sortOrder === 'desc' }"></text></view></view></view><!-- 商品列表 --><recycle-listclass="product-container":items="sortedProducts":item-size="220"key-field="id"@scroll="handleScroll"><template #default="{ item }"><view class="product-item"><view class="product-image"><lazy-image:src="item.imageUrl":aspect-ratio="1"loading="lazy"@load="handleImageLoad"/><view class="product-tags" v-if="item.tags?.length"><text v-for="tag in item.tags" :key="tag.id"class="tag":class="tag.type">{{ tag.text }}</text></view></view><view class="product-info"><text class="product-name">{{ item.name }}</text><text class="product-desc">{{ item.description }}</text><view class="product-meta"><view class="price"><text class="currency">¥</text><text class="amount">{{ formatPrice(item.price) }}</text></view><view class="sales"><text class="count">{{ formatNumber(item.salesCount) }}</text><text class="unit">已售</text></view></view><view class="product-rating" v-if="item.rating"><rate-display :value="item.rating" :size="24" /><text class="rating-count">({{ formatNumber(item.ratingCount) }})</text></view></view></view></template></recycle-list><!-- 加载状态 --><view class="loading-status"><template v-if="isLoading"><loading-spinner size="24" /><text>加载中...</text></template><template v-else-if="hasMore"><text>上拉加载更多</text></template><template v-else><text>没有更多数据了</text></template></view></view>
</template><script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useProductStore } from '@/stores/product'
import { usePreferenceStore } from '@/stores/preference'
import { debounce } from 'lodash-es'
import type { SortOption, Product, SortOrder } from '@/types'// 排序选项配置
const sortOptions: SortOption[] = [{ label: '综合', value: 'comprehensive', sortable: false },{ label: '销量', value: 'sales', sortable: true },{ label: '价格', value: 'price', sortable: true },{ label: '好评', value: 'rating', sortable: true },{ label: '智能', value: 'smart', sortable: false }
]// 状态管理
const productStore = useProductStore()
const preferenceStore = usePreferenceStore()// 响应式数据
const currentSort = ref('comprehensive')
const sortOrder = ref<SortOrder>('desc')
const isLoading = ref(false)
const hasMore = ref(true)
const pageSize = 20
const currentPage = ref(1)// 计算属性:排序后的商品列表
const sortedProducts = computed(() => {const products = [...productStore.products]switch (currentSort.value) {case 'price':return products.sort((a, b) => {return sortOrder.value === 'asc' ? a.price - b.price : b.price - a.price})case 'sales':return products.sort((a, b) => {return sortOrder.value === 'asc'? a.salesCount - b.salesCount: b.salesCount - a.salesCount})case 'rating':return products.sort((a, b) => {return sortOrder.value === 'asc'? a.rating - b.rating: b.rating - a.rating})case 'smart':return applySortingAlgorithm(products)default:return applyComprehensiveSorting(products)}
})// 智能排序算法
const applySortingAlgorithm = (products: Product[]) => {const userPreferences = preferenceStore.preferencesreturn products.sort((a, b) => {// 计算商品得分const scoreA = calculateProductScore(a, userPreferences)const scoreB = calculateProductScore(b, userPreferences)return scoreB - scoreA})
}// 商品得分计算
const calculateProductScore = (product: Product, preferences: any) => {let score = 0// 基础分数:销量、评分、价格等维度score += product.salesCount * 0.4score += product.rating * 0.3score += (1 / product.price) * 0.2// 用户偏好加权if (preferences.categories?.includes(product.category)) {score *= 1.2}if (preferences.brands?.includes(product.brand)) {score *= 1.1}// 时间衰减因子const daysSincePublish = calculateDaysDiff(product.publishTime, new Date())score *= Math.exp(-0.01 * daysSincePublish)return score
}// 综合排序实现
const applyComprehensiveSorting = (products: Product[]) => {return products.sort((a, b) => {// 多维度权重计算const weightA = calculateWeight(a)const weightB = calculateWeight(b)return weightB - weightA})
}// 权重计算函数
const calculateWeight = (product: Product) => {const salesWeight = 0.35const ratingWeight = 0.25const priceWeight = 0.2const timeWeight = 0.2const normalizedSales = normalize(product.salesCount, 0, 10000)const normalizedRating = product.rating / 5const normalizedPrice = 1 - normalize(product.price, 0, 10000)const normalizedTime = normalize(new Date(product.publishTime).getTime(),new Date().getTime() - 30 * 24 * 60 * 60 * 1000,new Date().getTime())return (salesWeight * normalizedSales +ratingWeight * normalizedRating +priceWeight * normalizedPrice +timeWeight * normalizedTime)
}// 数值归一化
const normalize = (value: number, min: number, max: number) => {return (value - min) / (max - min)
}// 排序处理
const handleSort = (sortType: string) => {if (currentSort.value === sortType && sortOptions.find(opt => opt.value === sortType)?.sortable) {// 切换排序方向sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'} else {currentSort.value = sortTypesortOrder.value = 'desc'}// 重置分页currentPage.value = 1hasMore.value = true// 重新加载数据loadProducts()
}// 加载商品数据
const loadProducts = async () => {if (isLoading.value || !hasMore.value) returntry {isLoading.value = trueconst params = {page: currentPage.value,pageSize,sortBy: currentSort.value,sortOrder: sortOrder.value}const { items, total } = await productStore.fetchProducts(params)hasMore.value = items.length === pageSizecurrentPage.value++} catch (error) {uni.showToast({title: '加载失败,请重试',icon: 'none'})} finally {isLoading.value = false}
}// 滚动加载
const handleScroll = debounce((e: any) => {const { scrollHeight, scrollTop, clientHeight } = e.detailif (scrollHeight - scrollTop - clientHeight < 50) {loadProducts()}
}, 100)// 图片加载优化
const handleImageLoad = (e: any) => {// 图片加载完成后的处理逻辑
}// 工具函数
const formatPrice = (price: number) => {return price.toFixed(2)
}const formatNumber = (num: number) => {return num >= 10000 ? (num / 10000).toFixed(1) + '万': num.toString()
}const calculateDaysDiff = (date1: Date, date2: Date) => {return Math.floor((date2.getTime() - date1.getTime()) / (1000 * 60 * 60 * 24))
}// 生命周期
onMounted(() => {loadProducts()
})// 监听排序变化
watch([currentSort, sortOrder], () => {// 保存用户排序偏好preferenceStore.saveSortPreference({type: currentSort.value,order: sortOrder.value})
})
</script><style lang="scss">
.product-list {height: 100%;background: #f5f5f5;.sort-toolbar {display: flex;align-items: center;height: 88rpx;background: #fff;padding: 0 20rpx;position: sticky;top: 0;z-index: 100;.sort-item {flex: 1;display: flex;align-items: center;justify-content: center;font-size: 28rpx;color: #333;position: relative;&.active {color: var(--primary-color);font-weight: 500;}.sort-icon {display: flex;flex-direction: column;margin-left: 4rpx;.iconfont {font-size: 20rpx;color: #999;line-height: 1;&.active {color: var(--primary-color);}}}}}.product-container {height: calc(100% - 88rpx);padding: 20rpx;.product-item {background: #fff;border-radius: 12rpx;margin-bottom: 20rpx;overflow: hidden;.product-image {position: relative;width: 100%;.product-tags {position: absolute;top: 12rpx;left: 12rpx;display: flex;flex-wrap: wrap;gap: 8rpx;.tag {padding: 4rpx 12rpx;font-size: 20rpx;color: #fff;border-radius: 4rpx;&.hot {background: #ff4d4f;}&.new {background: #52c41a;}&.promotion {background: #1890ff;}}}}.product-info {padding: 20rpx;.product-name {font-size: 28rpx;color: #333;font-weight: 500;line-height: 1.4;margin-bottom: 8rpx;}.product-desc {font-size: 24rpx;color: #666;line-height: 1.4;margin-bottom: 16rpx;}.product-meta {display: flex;align-items: center;justify-content: space-between;margin-bottom: 12rpx;.price {color: #ff4d4f;.currency {font-size: 24rpx;}.amount {font-size: 32rpx;font-weight: 500;}}.sales {font-size: 24rpx;color: #999;.count {margin-right: 4rpx;}}}.product-rating {display: flex;align-items: center;.rating-count {font-size: 24rpx;color: #999;margin-left: 8rpx;}}}}}.loading-status {display: flex;align-items: center;justify-content: center;height: 80rpx;color: #999;font-size: 24rpx;.loading-spinner {margin-right: 8rpx;}}
}// 深色模式适配
@media (prefers-color-scheme: dark) {.product-list {background: #1a1a1a;.sort-toolbar {background: #2c2c2c;.sort-item {color: #fff;}}.product-container {.product-item {background: #2c2c2c;.product-info {.product-name {color: #fff;}.product-desc {color: rgba(255, 255, 255, 0.65);}}}}}
}
</style>
2. 状态管理
// stores/product.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Product, ProductQueryParams } from '@/types'export const useProductStore = defineStore('product', () => {// 状态定义const products = ref<Product[]>([])const total = ref(0)// 获取商品列表const fetchProducts = async (params: ProductQueryParams) => {try {const response = await uni.request({url: '/api/products',method: 'GET',data: params})const { items, total: totalCount } = response.dataif (params.page === 1) {products.value = items} else {products.value.push(...items)}total.value = totalCountreturn {items,total: totalCount}} catch (error) {console.error('Failed to fetch products:', error)throw error}}// 更新商品数据const updateProduct = (productId: string, data: Partial<Product>) => {const index = products.value.findIndex(p => p.id === productId)if (index > -1) {products.value[index] = { ...products.value[index], ...data }}}return {products,total,fetchProducts,updateProduct}
})
3. 自定义图片懒加载指令
// directives/lazy-image.ts
import { DirectiveBinding } from 'vue'interface LazyImageState {loaded: booleanerror: booleanloading: booleanattempt: number
}export const lazyImage = {mounted(el: HTMLImageElement, binding: DirectiveBinding) {const state: LazyImageState = {loaded: false,error: false,loading: false,attempt: 0}const observer = new IntersectionObserver(entries => {entries.forEach(entry => {if (entry.isIntersecting) {loadImage(el, binding.value, state)observer.unobserve(el)}})}, {rootMargin: '50px'})observer.observe(el)}
}function loadImage(el: HTMLImageElement, src: string, state: LazyImageState) {if (state.loading || state.loaded || state.error) returnstate.loading = trueconst img = new Image()img.src = srcimg.onload = () => {el.src = srcstate.loading = falsestate.loaded = true}img.onerror = () => {state.loading = falsestate.error = truestate.attempt++if (state.attempt <= 3) {setTimeout(() => {loadImage(el, src, state)}, 1000 * state.attempt)}}
}
HarmonyOS平台优化
1. 性能优化
-
列表渲染优化
- 使用虚拟列表
- 图片懒加载
- 数据分页
-
动画性能
- 使用transform代替位置属性
- 开启硬件加速
- 避免重排重绘
-
内存管理
- 及时释放不需要的资源
- 控制图片缓存大小
- 优化大列表数据结构
2. 交互优化
-
手势操作
- 支持下拉刷新
- 流畅的滚动体验
- 顺滑的动画效果
-
视觉反馈
- 加载状态提示
- 错误处理展示
- 操作结果反馈
最佳实践建议
-
数据处理
- 合理的数据结构设计
- 高效的排序算法
- 本地数据缓存策略
-
用户体验
- 智能的排序推荐
- 流畅的滚动体验
- 清晰的视觉反馈
-
代码质量
- TypeScript类型约束
- 组件化开发
- 统一的错误处理
总结
通过本文的实践,我们实现了一个功能完备、性能优异的商品展示系统。该方案具有以下特点:
- 智能的排序算法
- 高效的性能表现
- 流畅的用户体验
- 完善的平台适配
- 可扩展的架构设计
希望本文的内容能够帮助开发者更好地实现商品展示相关功能,同时为HarmonyOS平台的应用开发提供参考。
参考资源
- UniApp官方文档
- HarmonyOS设计规范
- 前端性能优化指南
- 移动端交互设计指南