feat: 初次提交我的项目代码

This commit is contained in:
2026-01-22 17:07:39 +08:00
parent 75fad97d5d
commit 73498128dd
39 changed files with 21439 additions and 835 deletions

View File

@@ -0,0 +1,526 @@
<!-- 商品分类页面 -->
<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>