526 lines
11 KiB
Plaintext
526 lines
11 KiB
Plaintext
<!-- 商品分类页面 -->
|
||
<template>
|
||
<view class="category-page">
|
||
<!-- 搜索栏 -->
|
||
<view class="search-header">
|
||
<view class="search-box" @click="goToSearch">
|
||
<text class="search-icon">🔍</text>
|
||
<text class="search-placeholder">搜索商品</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="category-container">
|
||
<!-- 左侧分类导航 -->
|
||
<scroll-view class="category-nav" scroll-y>
|
||
<view v-for="category in categories"
|
||
:key="category.id"
|
||
:class="['nav-item', { active: activeCategoryId === category.id }]"
|
||
@click="selectCategory(category)">
|
||
<text class="nav-text">{{ category.name }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 右侧内容区域 -->
|
||
<scroll-view class="category-content" scroll-y>
|
||
<!-- 当前分类的banner -->
|
||
<view v-if="currentCategory" class="category-banner">
|
||
<image class="banner-image" :src="currentCategory.image_url || '/static/default-banner.png'" />
|
||
</view>
|
||
|
||
<!-- 子分类 -->
|
||
<view v-if="subCategories.length > 0" class="sub-category-section">
|
||
<view class="section-title">{{ currentCategory?.name }}分类</view>
|
||
<view class="sub-category-grid">
|
||
<view v-for="subCategory in subCategories"
|
||
:key="subCategory.id"
|
||
class="sub-category-item"
|
||
@click="navigateToSubCategory(subCategory)">
|
||
<image class="sub-category-icon" :src="subCategory.icon_url || '/static/default-category.png'" />
|
||
<text class="sub-category-name">{{ subCategory.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 品牌专区 -->
|
||
<view v-if="brands.length > 0" class="brand-section">
|
||
<view class="section-header">
|
||
<text class="section-title">品牌推荐</text>
|
||
<text class="more-btn" @click="viewAllBrands">更多 ›</text>
|
||
</view>
|
||
<scroll-view class="brand-scroll" scroll-x>
|
||
<view class="brand-list">
|
||
<view v-for="brand in brands"
|
||
:key="brand.id"
|
||
class="brand-item"
|
||
@click="viewBrandProducts(brand)">
|
||
<image class="brand-logo" :src="brand.logo_url || '/static/default-brand.png'" />
|
||
<text class="brand-name">{{ brand.name }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 热销商品 -->
|
||
<view v-if="hotProducts.length > 0" class="hot-products-section">
|
||
<view class="section-title">热销商品</view>
|
||
<view class="hot-products-grid">
|
||
<view v-for="product in hotProducts"
|
||
:key="product.id"
|
||
class="product-item"
|
||
@click="viewProductDetail(product)">
|
||
<image class="product-image" :src="getProductFirstImage(product)" />
|
||
<view class="product-info">
|
||
<text class="product-name">{{ product.name }}</text>
|
||
<view class="product-price-row">
|
||
<text class="current-price">¥{{ product.price }}</text>
|
||
<text v-if="product.original_price && product.original_price > product.price"
|
||
class="original-price">¥{{ product.original_price }}</text>
|
||
</view>
|
||
<view class="sales-info">
|
||
<text class="sales-text">已售{{ product.sales }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view v-if="isLoadingProducts" class="loading-more">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, onMounted, watch } from 'vue'
|
||
import type { ProductType } from '@/types/mall-types.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
type CategoryType = {
|
||
id: string
|
||
name: string
|
||
parent_id: string | null
|
||
icon_url: string | null
|
||
image_url: string | null
|
||
sort_order: number
|
||
is_active: boolean
|
||
}
|
||
|
||
type BrandType = {
|
||
id: string
|
||
name: string
|
||
logo_url: string | null
|
||
description: string | null
|
||
}
|
||
|
||
const categories = ref<Array<CategoryType>>([])
|
||
const activeCategoryId = ref<string>('')
|
||
const currentCategory = ref<CategoryType | null>(null)
|
||
const subCategories = ref<Array<CategoryType>>([])
|
||
const brands = ref<Array<BrandType>>([])
|
||
const hotProducts = ref<Array<ProductType>>([])
|
||
const isLoadingProducts = ref<boolean>(false)
|
||
|
||
// 监听分类切换
|
||
watch(activeCategoryId, (newId) => {
|
||
if (newId) {
|
||
loadCategoryData(newId)
|
||
}
|
||
})
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
loadCategories()
|
||
})
|
||
|
||
// 加载一级分类
|
||
const loadCategories = async () => {
|
||
try {
|
||
const { data, error } = await supa
|
||
.from('categories')
|
||
.select('*')
|
||
.eq('is_active', true)
|
||
.is('parent_id', null)
|
||
.order('sort_order', { ascending: true })
|
||
|
||
if (error !== null) {
|
||
console.error('加载分类失败:', error)
|
||
return
|
||
}
|
||
|
||
categories.value = data ?? []
|
||
|
||
// 默认选中第一个分类
|
||
if (categories.value.length > 0) {
|
||
activeCategoryId.value = categories.value[0].id
|
||
}
|
||
} catch (err) {
|
||
console.error('加载分类异常:', err)
|
||
}
|
||
}
|
||
|
||
// 加载分类数据
|
||
const loadCategoryData = async (categoryId: string) => {
|
||
// 1. 获取当前分类信息
|
||
try {
|
||
const { data: categoryData, error: categoryError } = await supa
|
||
.from('categories')
|
||
.select('*')
|
||
.eq('id', categoryId)
|
||
.single()
|
||
|
||
if (categoryError !== null) {
|
||
console.error('加载分类详情失败:', categoryError)
|
||
return
|
||
}
|
||
|
||
currentCategory.value = categoryData
|
||
|
||
// 2. 加载子分类
|
||
const { data: subData, error: subError } = await supa
|
||
.from('categories')
|
||
.select('*')
|
||
.eq('parent_id', categoryId)
|
||
.eq('is_active', true)
|
||
.order('sort_order', { ascending: true })
|
||
|
||
if (subError !== null) {
|
||
console.error('加载子分类失败:', subError)
|
||
} else {
|
||
subCategories.value = subData ?? []
|
||
}
|
||
|
||
// 3. 加载品牌
|
||
const { data: brandData, error: brandError } = await supa
|
||
.from('brands')
|
||
.select('*')
|
||
.eq('is_active', true)
|
||
.order('sort_order', { ascending: true })
|
||
.limit(8)
|
||
|
||
if (brandError !== null) {
|
||
console.error('加载品牌失败:', brandError)
|
||
} else {
|
||
brands.value = brandData ?? []
|
||
}
|
||
|
||
// 4. 加载热销商品
|
||
loadHotProducts(categoryId)
|
||
} catch (err) {
|
||
console.error('加载分类数据异常:', err)
|
||
}
|
||
}
|
||
|
||
// 加载热销商品
|
||
const loadHotProducts = async (categoryId: string) => {
|
||
isLoadingProducts.value = true
|
||
|
||
try {
|
||
const { data, error } = await supa
|
||
.from('products')
|
||
.select('*')
|
||
.eq('status', 1)
|
||
.eq('category_id', categoryId)
|
||
.order('sales', { ascending: false })
|
||
.limit(12)
|
||
|
||
if (error !== null) {
|
||
console.error('加载热销商品失败:', error)
|
||
return
|
||
}
|
||
|
||
hotProducts.value = data ?? []
|
||
} catch (err) {
|
||
console.error('加载热销商品异常:', err)
|
||
} finally {
|
||
isLoadingProducts.value = false
|
||
}
|
||
}
|
||
|
||
// 获取商品第一张图片
|
||
const getProductFirstImage = (product: ProductType): string => {
|
||
return product.images?.[0] || '/static/default-product.png'
|
||
}
|
||
|
||
// 选择分类
|
||
const selectCategory = (category: CategoryType) => {
|
||
activeCategoryId.value = category.id
|
||
}
|
||
|
||
// 导航到子分类
|
||
const navigateToSubCategory = (subCategory: CategoryType) => {
|
||
// 可以跳转到子分类的商品列表页
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/product-list?categoryId=${subCategory.id}&title=${encodeURIComponent(subCategory.name)}`
|
||
})
|
||
}
|
||
|
||
// 查看品牌商品
|
||
const viewBrandProducts = (brand: BrandType) => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/product-list?brandId=${brand.id}&title=${encodeURIComponent(brand.name)}`
|
||
})
|
||
}
|
||
|
||
// 查看商品详情
|
||
const viewProductDetail = (product: ProductType) => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/product-detail?id=${product.id}`
|
||
})
|
||
}
|
||
|
||
// 查看所有品牌
|
||
const viewAllBrands = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/brands'
|
||
})
|
||
}
|
||
|
||
// 跳转到搜索页
|
||
const goToSearch = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/search'
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.category-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.search-header {
|
||
background-color: #ffffff;
|
||
padding: 10px 15px;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.search-box {
|
||
background-color: #f8f8f8;
|
||
border-radius: 20px;
|
||
padding: 10px 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-icon {
|
||
color: #999999;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.search-placeholder {
|
||
color: #999999;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.category-container {
|
||
flex: 1;
|
||
display: flex;
|
||
}
|
||
|
||
.category-nav {
|
||
width: 100px;
|
||
background-color: #f8f8f8;
|
||
border-right: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.nav-item {
|
||
padding: 15px 10px;
|
||
text-align: center;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.nav-item.active {
|
||
background-color: #ffffff;
|
||
color: #007aff;
|
||
border-left: 3px solid #007aff;
|
||
}
|
||
|
||
.nav-text {
|
||
font-size: 14px;
|
||
color: #333333;
|
||
}
|
||
|
||
.nav-item.active .nav-text {
|
||
color: #007aff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.category-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.category-banner {
|
||
padding: 15px;
|
||
}
|
||
|
||
.banner-image {
|
||
width: 100%;
|
||
height: 120px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.sub-category-section,
|
||
.brand-section,
|
||
.hot-products-section {
|
||
padding: 15px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.more-btn {
|
||
color: #007aff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.sub-category-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin: 0 -5px;
|
||
}
|
||
|
||
.sub-category-item {
|
||
width: 33.33%;
|
||
padding: 0 5px;
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.sub-category-icon {
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 25px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.sub-category-name {
|
||
font-size: 12px;
|
||
color: #666666;
|
||
text-align: center;
|
||
}
|
||
|
||
.brand-scroll {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.brand-list {
|
||
display: inline-flex;
|
||
}
|
||
|
||
.brand-item {
|
||
width: 80px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.brand-logo {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
border: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.brand-name {
|
||
font-size: 12px;
|
||
color: #333333;
|
||
text-align: center;
|
||
}
|
||
|
||
.hot-products-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin: 0 -5px;
|
||
}
|
||
|
||
.product-item {
|
||
width: 50%;
|
||
padding: 0 5px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.product-image {
|
||
width: 100%;
|
||
height: 100px;
|
||
border-radius: 5px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.product-info {
|
||
padding: 0 5px;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 13px;
|
||
color: #333333;
|
||
line-height: 1.4;
|
||
margin-bottom: 8px;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.product-price-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.current-price {
|
||
font-size: 14px;
|
||
color: #ff4757;
|
||
font-weight: bold;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.original-price {
|
||
font-size: 12px;
|
||
color: #999999;
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.sales-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.sales-text {
|
||
font-size: 11px;
|
||
color: #999999;
|
||
}
|
||
|
||
.loading-more {
|
||
padding: 20px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loading-text {
|
||
color: #999999;
|
||
font-size: 14px;
|
||
}
|
||
</style> |