pet/pages/review/review.vue

1105 lines
23 KiB
Vue

<template>
<view class="review-container page-container-with-bg">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar">
<u-search
v-model="searchKeyword"
placeholder="搜索产品名称或品牌"
:show-action="false"
bg-color="rgba(255, 255, 255, 0.9)"
@search="onSearch"
@custom="onSearch"
></u-search>
</view>
</view>
<!-- 筛选区域 -->
<view class="filter-section">
<view class="filter-tabs">
<scroll-view class="filter-scroll" scroll-x>
<view class="filter-list">
<view
class="filter-item"
:class="{ active: currentPetType === 'all' }"
@click="setPetType('all')"
>
<text class="filter-text">全部</text>
</view>
<view
class="filter-item"
v-for="type in petTypes"
:key="type.value"
:class="{ active: currentPetType === type.value }"
@click="setPetType(type.value)"
>
<text class="filter-icon">{{ type.icon }}</text>
<text class="filter-text">{{ type.label }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="filter-categories">
<scroll-view class="category-scroll" scroll-x>
<view class="category-list">
<view
class="category-item"
:class="{ active: currentCategory === 'all' }"
@click="setCategory('all')"
>
<text class="category-text">全部</text>
</view>
<view
class="category-item"
v-for="category in productCategories"
:key="category.value"
:class="{ active: currentCategory === category.value }"
@click="setCategory(category.value)"
>
<text class="category-text">{{ category.label }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 排序和视图切换 -->
<view class="toolbar-section">
<view class="sort-options">
<view
class="sort-item"
:class="{ active: currentSort === 'rating' }"
@click="setSort('rating')"
>
<text class="sort-text">评分</text>
</view>
<view
class="sort-item"
:class="{ active: currentSort === 'price' }"
@click="setSort('price')"
>
<text class="sort-text">价格</text>
</view>
<view
class="sort-item"
:class="{ active: currentSort === 'hot' }"
@click="setSort('hot')"
>
<text class="sort-text">热度</text>
</view>
</view>
<view class="view-toggle">
<view
class="toggle-item"
:class="{ active: viewMode === 'grid' }"
@click="setViewMode('grid')"
>
<text class="toggle-icon">⊞</text>
</view>
<view
class="toggle-item"
:class="{ active: viewMode === 'list' }"
@click="setViewMode('list')"
>
<text class="toggle-icon">☰</text>
</view>
</view>
</view>
<!-- 产品列表 -->
<view class="products-section">
<view class="products-grid" :class="viewMode" v-if="filteredProducts.length > 0">
<view
class="product-card"
v-for="product in filteredProducts"
:key="product.id"
@click="viewProductDetail(product)"
>
<view class="product-image">
<image class="product-img" :src="product.image" mode="aspectFill" />
<view class="product-badge" v-if="product.badge">
<text class="badge-text">{{ product.badge }}</text>
</view>
<view class="product-favorite" @click.stop="toggleFavorite(product)">
<text class="favorite-icon" :class="{ active: product.isFavorite }">♥</text>
</view>
</view>
<view class="product-info">
<view class="product-brand">{{ product.brand }}</view>
<view class="product-name">{{ product.name }}</view>
<view class="product-rating">
<view class="rating-stars">
<text
class="star"
v-for="i in 5"
:key="i"
:class="{ active: i <= product.rating }"
>★</text>
</view>
<text class="rating-score">{{ product.rating }}</text>
<text class="rating-count">({{ product.reviewCount }})</text>
</view>
<view class="product-price">
<text class="price-range">¥{{ product.priceRange }}</text>
</view>
<view class="product-tags" v-if="viewMode === 'list'">
<text
class="product-tag"
v-for="tag in product.tags"
:key="tag"
>{{ tag }}</text>
</view>
</view>
</view>
</view>
<view class="empty-products" v-else>
<view class="empty-icon">🔍</view>
<view class="empty-text">暂无相关产品评测</view>
<view class="empty-desc">试试调整筛选条件或搜索其他关键词</view>
</view>
</view>
<!-- 浮动操作按钮 -->
<view class="fab-container">
<view class="fab-button" @click="showFilterModal">
<text class="fab-icon">🔧</text>
</view>
</view>
<!-- 筛选弹窗 -->
<u-modal
:show="showFilter"
title="高级筛选"
@confirm="applyFilter"
@cancel="showFilter = false"
>
<view class="filter-modal">
<view class="filter-group">
<text class="group-title">品牌筛选</text>
<view class="brand-options">
<view
class="brand-item"
v-for="brand in brands"
:key="brand"
:class="{ active: selectedBrands.includes(brand) }"
@click="toggleBrand(brand)"
>
<text class="brand-text">{{ brand }}</text>
</view>
</view>
</view>
<view class="filter-group">
<text class="group-title">价格区间</text>
<view class="price-options">
<view
class="price-item"
v-for="price in priceRanges"
:key="price.value"
:class="{ active: selectedPriceRange === price.value }"
@click="setPriceRange(price.value)"
>
<text class="price-text">{{ price.label }}</text>
</view>
</view>
</view>
</view>
</u-modal>
</view>
</template>
<script>
import { reactive, ref, onMounted, computed } from 'vue'
export default {
name: 'ReviewPage',
setup() {
// 响应式数据
const searchKeyword = ref('')
const currentPetType = ref('all')
const currentCategory = ref('all')
const currentSort = ref('rating')
const viewMode = ref('grid')
const showFilter = ref(false)
const selectedBrands = ref([])
const selectedPriceRange = ref('all')
const productsList = ref([])
const favoritesList = ref([])
// 筛选选项数据
const petTypes = ref([
{ value: 'cat', label: '猫咪', icon: '🐱' },
{ value: 'dog', label: '狗狗', icon: '🐶' }
])
const productCategories = ref([
{ value: 'food', label: '主粮' },
{ value: 'snack', label: '零食' },
{ value: 'litter', label: '猫砂' },
{ value: 'toy', label: '玩具' },
{ value: 'supplies', label: '用品' },
{ value: 'health', label: '保健品' }
])
const brands = ref([
'皇家', '希尔斯', '渴望', '蓝氏', '冠能', '爱肯拿', '纽翠斯', '百利', '素力高', '网易严选'
])
const priceRanges = ref([
{ value: 'all', label: '不限' },
{ value: '0-50', label: '50元以下' },
{ value: '50-100', label: '50-100元' },
{ value: '100-200', label: '100-200元' },
{ value: '200-500', label: '200-500元' },
{ value: '500+', label: '500元以上' }
])
// 计算属性
const filteredProducts = computed(() => {
let filtered = productsList.value
// 搜索筛选
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
filtered = filtered.filter(product =>
product.name.toLowerCase().includes(keyword) ||
product.brand.toLowerCase().includes(keyword)
)
}
// 宠物类型筛选
if (currentPetType.value !== 'all') {
filtered = filtered.filter(product => product.petType === currentPetType.value)
}
// 产品类别筛选
if (currentCategory.value !== 'all') {
filtered = filtered.filter(product => product.category === currentCategory.value)
}
// 品牌筛选
if (selectedBrands.value.length > 0) {
filtered = filtered.filter(product => selectedBrands.value.includes(product.brand))
}
// 价格筛选
if (selectedPriceRange.value !== 'all') {
filtered = filtered.filter(product => {
const price = parseFloat(product.priceRange.split('-')[0])
switch (selectedPriceRange.value) {
case '0-50':
return price < 50
case '50-100':
return price >= 50 && price < 100
case '100-200':
return price >= 100 && price < 200
case '200-500':
return price >= 200 && price < 500
case '500+':
return price >= 500
default:
return true
}
})
}
// 排序
filtered.sort((a, b) => {
switch (currentSort.value) {
case 'rating':
return b.rating - a.rating
case 'price':
const priceA = parseFloat(a.priceRange.split('-')[0])
const priceB = parseFloat(b.priceRange.split('-')[0])
return priceA - priceB
case 'hot':
return b.reviewCount - a.reviewCount
default:
return 0
}
})
return filtered
})
// 生命周期
onMounted(() => {
loadProducts()
loadFavorites()
})
// 方法定义
const loadProducts = () => {
// 模拟产品数据
const mockProducts = [
{
id: 'p1',
name: '成猫粮 室内猫配方',
brand: '皇家',
category: 'food',
petType: 'cat',
rating: 4.8,
reviewCount: 1256,
priceRange: '89-156',
image: '/static/product1.jpg',
badge: '热销',
tags: ['室内猫', '营养均衡', '毛球护理'],
isFavorite: false
},
{
id: 'p2',
name: '幼犬粮 小型犬专用',
brand: '希尔斯',
category: 'food',
petType: 'dog',
rating: 4.7,
reviewCount: 892,
priceRange: '128-268',
image: '/static/product2.jpg',
badge: '新品',
tags: ['小型犬', '幼犬', '易消化'],
isFavorite: false
},
{
id: 'p3',
name: '无尘豆腐猫砂',
brand: '网易严选',
category: 'litter',
petType: 'cat',
rating: 4.6,
reviewCount: 2341,
priceRange: '45-78',
image: '/static/product3.jpg',
badge: '',
tags: ['无尘', '结团好', '除臭'],
isFavorite: false
},
{
id: 'p4',
name: '冻干鸡肉粒',
brand: '蓝氏',
category: 'snack',
petType: 'cat',
rating: 4.9,
reviewCount: 567,
priceRange: '35-68',
image: '/static/product4.jpg',
badge: '推荐',
tags: ['冻干', '高蛋白', '训练奖励'],
isFavorite: false
},
{
id: 'p5',
name: '智能逗猫棒',
brand: '小佩',
category: 'toy',
petType: 'cat',
rating: 4.5,
reviewCount: 423,
priceRange: '89-129',
image: '/static/product5.jpg',
badge: '',
tags: ['智能', '自动', '互动'],
isFavorite: false
},
{
id: 'p6',
name: '关节保健软糖',
brand: '渴望',
category: 'health',
petType: 'dog',
rating: 4.4,
reviewCount: 234,
priceRange: '156-298',
image: '/static/product6.jpg',
badge: '',
tags: ['关节保健', '老年犬', '软糖'],
isFavorite: false
}
]
productsList.value = mockProducts
uni.setStorageSync('reviewProducts', mockProducts)
}
const loadFavorites = () => {
try {
const saved = uni.getStorageSync('reviewFavorites') || []
favoritesList.value = saved
// 更新产品收藏状态
productsList.value.forEach(product => {
product.isFavorite = favoritesList.value.includes(product.id)
})
} catch (error) {
console.error('加载收藏失败:', error)
}
}
const saveFavorites = () => {
uni.setStorageSync('reviewFavorites', favoritesList.value)
}
const onSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const setPetType = (type) => {
currentPetType.value = type
}
const setCategory = (category) => {
currentCategory.value = category
}
const setSort = (sort) => {
currentSort.value = sort
}
const setViewMode = (mode) => {
viewMode.value = mode
}
const showFilterModal = () => {
showFilter.value = true
}
const toggleBrand = (brand) => {
const index = selectedBrands.value.indexOf(brand)
if (index > -1) {
selectedBrands.value.splice(index, 1)
} else {
selectedBrands.value.push(brand)
}
}
const setPriceRange = (range) => {
selectedPriceRange.value = range
}
const applyFilter = () => {
showFilter.value = false
}
const toggleFavorite = (product) => {
const index = favoritesList.value.indexOf(product.id)
if (index > -1) {
favoritesList.value.splice(index, 1)
product.isFavorite = false
} else {
favoritesList.value.push(product.id)
product.isFavorite = true
}
saveFavorites()
uni.showToast({
title: product.isFavorite ? '已收藏' : '已取消收藏',
icon: 'success'
})
}
const viewProductDetail = (product) => {
// 保存浏览历史
let history = uni.getStorageSync('reviewHistory') || []
const existIndex = history.findIndex(item => item.id === product.id)
if (existIndex > -1) {
history.splice(existIndex, 1)
}
history.unshift({
...product,
viewTime: new Date().toISOString()
})
if (history.length > 50) {
history = history.slice(0, 50)
}
uni.setStorageSync('reviewHistory', history)
// 跳转到详情页
uni.navigateTo({
url: `/pages/review/detail?id=${product.id}`,
fail: () => {
uni.showToast({
title: '详情页开发中',
icon: 'none'
})
}
})
}
return {
searchKeyword,
currentPetType,
currentCategory,
currentSort,
viewMode,
showFilter,
selectedBrands,
selectedPriceRange,
petTypes,
productCategories,
brands,
priceRanges,
filteredProducts,
onSearch,
setPetType,
setCategory,
setSort,
setViewMode,
showFilterModal,
toggleBrand,
setPriceRange,
applyFilter,
toggleFavorite,
viewProductDetail
}
}
}
</script>
<style lang="scss" scoped>
.review-container {
padding-bottom: 120rpx;
}
/* 搜索栏 */
.search-section {
padding: 20rpx 30rpx;
.search-bar {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 24rpx;
padding: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
}
}
/* 筛选区域 */
.filter-section {
margin: 0 30rpx 20rpx 30rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 20rpx;
padding: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
.filter-tabs {
margin-bottom: 16rpx;
.filter-scroll {
white-space: nowrap;
.filter-list {
display: flex;
gap: 12rpx;
.filter-item {
display: flex;
align-items: center;
padding: 12rpx 20rpx;
border-radius: 20rpx;
background: rgba(255, 138, 128, 0.1);
border: 2rpx solid transparent;
transition: all 0.3s ease;
white-space: nowrap;
&.active {
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-color: #FF8A80;
.filter-text,
.filter-icon {
color: white;
}
}
.filter-icon {
font-size: 24rpx;
margin-right: 6rpx;
}
.filter-text {
font-size: 24rpx;
color: #666666;
}
}
}
}
}
.filter-categories {
.category-scroll {
white-space: nowrap;
.category-list {
display: flex;
gap: 12rpx;
.category-item {
padding: 8rpx 16rpx;
border-radius: 16rpx;
background: rgba(255, 138, 128, 0.05);
border: 1rpx solid rgba(255, 138, 128, 0.2);
transition: all 0.3s ease;
white-space: nowrap;
&.active {
background: rgba(255, 138, 128, 0.2);
border-color: #FF8A80;
.category-text {
color: #FF8A80;
font-weight: 600;
}
}
.category-text {
font-size: 22rpx;
color: #666666;
}
}
}
}
}
}
/* 工具栏 */
.toolbar-section {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 30rpx 20rpx 30rpx;
padding: 16rpx 20rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
.sort-options {
display: flex;
gap: 20rpx;
.sort-item {
padding: 8rpx 16rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&.active {
background: rgba(255, 138, 128, 0.1);
.sort-text {
color: #FF8A80;
font-weight: 600;
}
}
.sort-text {
font-size: 24rpx;
color: #666666;
}
}
}
.view-toggle {
display: flex;
background: rgba(255, 138, 128, 0.1);
border-radius: 12rpx;
padding: 4rpx;
.toggle-item {
padding: 8rpx 12rpx;
border-radius: 8rpx;
transition: all 0.3s ease;
&.active {
background: #FF8A80;
.toggle-icon {
color: white;
}
}
.toggle-icon {
font-size: 20rpx;
color: #666666;
}
}
}
}
/* 产品列表 */
.products-section {
margin: 0 30rpx;
.products-grid {
&.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
}
&.list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.product-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.product-image {
position: relative;
.product-img {
width: 100%;
height: 200rpx;
}
.product-badge {
position: absolute;
top: 12rpx;
left: 12rpx;
padding: 4rpx 12rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 12rpx;
.badge-text {
font-size: 20rpx;
color: white;
font-weight: 500;
}
}
.product-favorite {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 40rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.favorite-icon {
font-size: 24rpx;
color: #CCCCCC;
transition: all 0.3s ease;
&.active {
color: #FF8A80;
}
}
}
}
.product-info {
padding: 16rpx;
.product-brand {
font-size: 20rpx;
color: #999999;
margin-bottom: 6rpx;
}
.product-name {
font-size: 26rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
line-height: 1.3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.product-rating {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.rating-stars {
margin-right: 8rpx;
.star {
font-size: 16rpx;
color: #DDDDDD;
&.active {
color: #FFD700;
}
}
}
.rating-score {
font-size: 20rpx;
color: #333333;
font-weight: 600;
margin-right: 6rpx;
}
.rating-count {
font-size: 18rpx;
color: #999999;
}
}
.product-price {
margin-bottom: 8rpx;
.price-range {
font-size: 24rpx;
color: #FF8A80;
font-weight: 600;
}
}
.product-tags {
display: flex;
flex-wrap: wrap;
gap: 6rpx;
.product-tag {
padding: 2rpx 8rpx;
background: rgba(255, 138, 128, 0.1);
border-radius: 8rpx;
font-size: 18rpx;
color: #FF8A80;
}
}
}
}
&.list .product-card {
.product-image {
.product-img {
height: 160rpx;
}
}
.product-info {
.product-name {
-webkit-line-clamp: 1;
}
}
}
}
}
/* 空状态 */
.empty-products {
text-align: center;
padding: 80rpx 40rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #666666;
margin-bottom: 12rpx;
font-weight: 500;
}
.empty-desc {
font-size: 24rpx;
color: #999999;
line-height: 1.5;
}
}
/* 浮动按钮 */
.fab-container {
position: fixed;
bottom: 120rpx;
right: 40rpx;
z-index: 100;
.fab-button {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.4);
transition: all 0.3s ease;
&:active {
transform: scale(0.9);
}
.fab-icon {
font-size: 32rpx;
color: white;
}
}
}
/* 筛选弹窗 */
.filter-modal {
padding: 20rpx 0;
.filter-group {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.group-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333333;
margin-bottom: 16rpx;
}
}
.brand-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.brand-item {
padding: 12rpx 20rpx;
border: 2rpx solid rgba(255, 138, 128, 0.2);
border-radius: 20rpx;
transition: all 0.3s ease;
&.active {
border-color: #FF8A80;
background: rgba(255, 138, 128, 0.1);
.brand-text {
color: #FF8A80;
font-weight: 600;
}
}
.brand-text {
font-size: 24rpx;
color: #666666;
}
}
}
.price-options {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.price-item {
padding: 12rpx 20rpx;
border: 2rpx solid rgba(255, 138, 128, 0.2);
border-radius: 20rpx;
transition: all 0.3s ease;
&.active {
border-color: #FF8A80;
background: rgba(255, 138, 128, 0.1);
.price-text {
color: #FF8A80;
font-weight: 600;
}
}
.price-text {
font-size: 24rpx;
color: #666666;
}
}
}
}
/* 响应式设计 */
@media (max-width: 375px) {
.review-container {
.search-section,
.filter-section,
.toolbar-section,
.products-section {
margin-left: 20rpx;
margin-right: 20rpx;
}
.products-grid.grid {
gap: 12rpx;
}
.fab-container {
right: 30rpx;
bottom: 100rpx;
.fab-button {
width: 60rpx;
height: 60rpx;
.fab-icon {
font-size: 28rpx;
}
}
}
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.review-container > view {
animation: fadeIn 0.5s ease-out;
}
.review-container > view:nth-child(1) { animation-delay: 0.1s; }
.review-container > view:nth-child(2) { animation-delay: 0.2s; }
.review-container > view:nth-child(3) { animation-delay: 0.3s; }
.review-container > view:nth-child(4) { animation-delay: 0.4s; }
.product-card {
animation: fadeIn 0.3s ease-out;
}
/* 交互反馈 */
.filter-item:active,
.category-item:active,
.sort-item:active,
.toggle-item:active,
.brand-item:active,
.price-item:active {
transform: scale(0.95);
}
.product-favorite:active {
transform: scale(0.8);
}
</style>