Files
akmon/pages/mall/consumer/index.uvue
2026-01-20 08:04:15 +08:00

833 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 消费者端首页 - 严格UTS Android规范 -->
<template>
<view class="consumer-home">
<!-- 顶部搜索栏 -->
<view class="header">
<view class="search-container">
<view class="search-box" @click="navigateToSearch">
<text class="search-icon">🔍</text>
<text class="search-placeholder">搜索商品</text>
</view>
<view class="cart-icon" @click="navigateToCart">
<text class="icon">🛒</text>
<text v-if="cartCount > 0" class="badge">{{ cartCount }}</text>
</view>
</view>
</view>
<scroll-view direction="vertical" class="main-content">
<!-- 轮播图 -->
<swiper class="banner-swiper" :autoplay="true" :interval="3000" :duration="500">
<swiper-item v-for="(banner, index) in bannerList" :key="index">
<image class="banner-image" :src="banner.image_url" mode="aspectFill" @click="handleBannerClick(banner)" />
</swiper-item>
</swiper>
<!-- 分类导航 -->
<view class="category-section">
<text class="section-title">商品分类</text>
<view class="category-grid">
<view v-for="(category, index) in categoryList" :key="category.id" class="category-item" @click="navigateToCategory(category)">
<image class="category-icon" :src="category.icon_url || '/static/default-category.png'" mode="aspectFit" />
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</view>
<!-- 优惠券中心 -->
<view class="coupon-section">
<view class="section-header">
<text class="section-title">优惠券</text>
<text class="more-btn" @click="navigateToCoupons">更多 </text>
</view>
<scroll-view direction="horizontal" class="coupon-scroll">
<view class="coupon-list">
<view v-for="(coupon, index) in couponList" :key="coupon.id" class="coupon-item" @click="receiveCoupon(coupon)">
<view class="coupon-left">
<text class="coupon-value">¥{{ coupon.discount_value }}</text>
<text class="coupon-condition">满{{ coupon.min_order_amount }}可用</text>
</view>
<view class="coupon-right">
<text class="coupon-name">{{ coupon.name }}</text>
<text class="coupon-expire">{{ formatExpireTime(coupon.end_time) }}</text>
<view class="receive-btn">
<text class="btn-text">立即领取</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 商品推荐 -->
<view class="product-section">
<text class="section-title">为你推荐</text>
<!-- #ifdef APP -->
<waterfall :column-count="2" :column-gap="10" :row-gap="10" class="product-waterfall">
<view v-for="(product, index) in productList" :key="product.id" class="product-card" @click="navigateToProduct(product)">
<image class="product-image" :src="getProductFirstImage(product)" mode="aspectFill" @error="handleImageError" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="price-row">
<text class="current-price">¥{{ product.price }}</text>
<text v-if="product.original_price !== null && product.original_price > product.price" class="original-price">¥{{ product.original_price }}</text>
</view>
<view class="sales-row">
<text class="sales-text">已售{{ product.sales }}件</text>
<view class="add-cart-btn" @click.stop="addToCart(product)">
<text class="add-text">+</text>
</view>
</view>
</view>
</view>
</waterfall>
<!-- #endif -->
<!-- #ifndef APP -->
<view class="product-grid">
<view v-for="(product, index) in productList" :key="product.id" class="product-card" @click="navigateToProduct(product)">
<image class="product-image" :src="getProductFirstImage(product)" mode="aspectFill" @error="handleImageError" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="price-row">
<text class="current-price">¥{{ product.price }}</text>
<text v-if="product.original_price !== null && product.original_price > product.price" class="original-price">¥{{ product.original_price }}</text>
</view>
<view class="sales-row">
<text class="sales-text">已售{{ product.sales }}件</text>
<view class="add-cart-btn" @click.stop="addToCart(product)">
<text class="add-text">+</text>
</view>
</view>
</view>
</view>
</view>
<!-- #endif -->
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
</scroll-view>
<!-- 底部导航 -->
<view class="bottom-navigation">
<view class="nav-item active">
<text class="nav-icon">🏠</text>
<text class="nav-text">首页</text>
</view>
<view class="nav-item" @click="navigateToCategory()">
<text class="nav-icon">📂</text>
<text class="nav-text">分类</text>
</view>
<view class="nav-item" @click="navigateToCart">
<text class="nav-icon">🛒</text>
<text class="nav-text">购物车</text>
</view>
<view class="nav-item" @click="navigateToProfile">
<text class="nav-icon">👤</text>
<text class="nav-text">我的</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import type { ProductType, CouponTemplateType } from '@/types/mall-types.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 数据定义
const bannerList = ref<Array<any>>([])
const categoryList = ref<Array<any>>([])
const couponList = ref<Array<CouponTemplateType>>([])
const productList = ref<Array<ProductType>>([])
const cartCount = ref<number>(0)
const isLoading = ref<boolean>(false)
const page = ref<number>(1)
const pageSize = ref<number>(20)
const hasMore = ref<boolean>(true)
// 页面加载
onMounted(() => {
loadBanners()
loadCategories()
loadCoupons()
loadProducts()
loadCartCount()
})
// 加载轮播图
const loadBanners = async () => {
try {
const { data, error } = await supa
.from('mall_banners')
.select('*')
.eq('status', 1)
.order('sort_order', { ascending: true })
.limit(5)
if (error !== null) {
console.error('加载轮播图失败:', error)
return
}
bannerList.value = data ?? []
} catch (err) {
console.error('加载轮播图异常:', err)
}
}
// 加载分类
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 })
.limit(8)
if (error !== null) {
console.error('加载分类失败:', error)
return
}
categoryList.value = data ?? []
} catch (err) {
console.error('加载分类异常:', err)
}
}
// 加载优惠券
const loadCoupons = async () => {
try {
const now = new Date().toISOString()
const { data, error } = await supa
.from('coupon_templates')
.select('*')
.eq('status', 1)
.gte('end_time', now)
.lte('start_time', now)
.limit(5)
if (error !== null) {
console.error('加载优惠券失败:', error)
return
}
couponList.value = data ?? []
} catch (err) {
console.error('加载优惠券异常:', err)
}
}
// 加载商品
const loadProducts = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const currentPage = loadMore ? page.value + 1 : 1
const { data, error } = await supa
.from('products')
.select('*')
.eq('status', 1)
.order('created_at', { ascending: false })
.range((currentPage - 1) * pageSize.value, currentPage * pageSize.value - 1)
if (error !== null) {
console.error('加载商品失败:', error)
isLoading.value = false
return
}
const newProducts = data ?? []
if (loadMore) {
for (let i: Int = 0; i < newProducts.length; i++) {
productList.value.push(newProducts[i])
}
page.value = currentPage
} else {
productList.value = newProducts
page.value = 1
}
hasMore.value = newProducts.length === pageSize.value
} catch (err) {
console.error('加载商品异常:', err)
} finally {
isLoading.value = false
}
}
// 加载购物车数量
const loadCartCount = async () => {
try {
// 这里应该从用户登录状态获取user_id
const userId = getCurrentUserId()
if (userId === null) {
return
}
const { data, error } = await supa
.from('shopping_cart')
.select('quantity')
.eq('user_id', userId)
if (error !== null) {
console.error('加载购物车数量失败:', error)
return
}
let total = 0
const cartItems = data ?? []
for (let i: Int = 0; i < cartItems.length; i++) {
total += cartItems[i].quantity
}
cartCount.value = total
} catch (err) {
console.error('加载购物车数量异常:', err)
}
}
// 获取当前用户ID (临时实现)
const getCurrentUserId = (): string | null => {
// 这里应该从全局状态或storage中获取
return null
}
// 获取商品第一张图片
const getProductFirstImage = (product: ProductType): string => {
if (product.images.length > 0) {
return product.images[0]
}
return '/static/default-product.png'
}
// 格式化过期时间
const formatExpireTime = (endTime: string): string => {
const date = new Date(endTime)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${month}.${day}到期`
}
// 图片加载错误处理
const handleImageError = (event: UniImageErrorEvent) => {
const target = event.target as UniImageElement
if (target !== null) {
target.src = '/static/default-product.png'
}
}
// 轮播图点击
const handleBannerClick = (banner: any) => {
if (banner.link_url !== null && banner.link_url !== '') {
// 处理轮播图跳转
console.log('点击轮播图:', banner.link_url)
}
}
// 领取优惠券
const receiveCoupon = async (coupon: CouponTemplateType) => {
const userId = getCurrentUserId()
if (userId === null) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
try {
// 检查是否已领取
const { data: existingCoupons, error: checkError } = await supa
.from('user_coupons')
.select('id')
.eq('user_id', userId)
.eq('template_id', coupon.id)
if (checkError !== null) {
console.error('检查优惠券失败:', checkError)
return
}
if (existingCoupons !== null && existingCoupons.length >= coupon.per_user_limit) {
uni.showToast({
title: '已达领取上限',
icon: 'none'
})
return
}
// 生成优惠券码
const couponCode = generateCouponCode()
const expireAt = coupon.end_time
const { error: insertError } = await supa
.from('user_coupons')
.insert({
user_id: userId,
template_id: coupon.id,
coupon_code: couponCode,
expire_at: expireAt
})
if (insertError !== null) {
console.error('领取优惠券失败:', insertError)
uni.showToast({
title: '领取失败',
icon: 'none'
})
return
}
uni.showToast({
title: '领取成功',
icon: 'success'
})
} catch (err) {
console.error('领取优惠券异常:', err)
uni.showToast({
title: '领取失败',
icon: 'none'
})
}
}
// 生成优惠券码
const generateCouponCode = (): string => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let result = ''
for (let i: Int = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
// 添加到购物车
const addToCart = async (product: ProductType) => {
const userId = getCurrentUserId()
if (userId === null) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
// 这里简化处理直接添加商品实际应该选择SKU
try {
const { error } = await supa
.from('shopping_cart')
.upsert({
user_id: userId,
product_id: product.id,
quantity: 1
})
if (error !== null) {
console.error('添加购物车失败:', error)
uni.showToast({
title: '添加失败',
icon: 'none'
})
return
}
cartCount.value++
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} catch (err) {
console.error('添加购物车异常:', err)
}
}
// 导航方法
const navigateToSearch = () => {
uni.navigateTo({
url: '/pages/mall/consumer/search'
})
}
const navigateToCart = () => {
uni.navigateTo({
url: '/pages/mall/consumer/cart'
})
}
const navigateToCategory = (category?: any) => {
const url = category !== null ?
`/pages/mall/consumer/category?id=${category.id}` :
'/pages/mall/consumer/category'
uni.navigateTo({
url: url
})
}
const navigateToProduct = (product: ProductType) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${product.id}`
})
}
const navigateToCoupons = () => {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
}
const navigateToProfile = () => {
uni.navigateTo({
url: '/pages/mall/consumer/profile'
})
}
</script>
<style scoped>
.consumer-home {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header {
background-color: #ffffff;
padding: 10px 15px;
border-bottom: 1px solid #e5e5e5;
}
.search-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.search-box {
flex: 1;
display: flex;
align-items: center;
background-color: #f8f8f8;
border-radius: 20px;
padding: 8px 15px;
margin-right: 15px;
}
.search-icon {
color: #999999;
margin-right: 8px;
}
.search-placeholder {
color: #999999;
font-size: 14px;
}
.cart-icon {
position: relative;
padding: 5px;
}
.icon {
font-size: 24px;
}
.badge {
position: absolute;
top: -2px;
right: -2px;
background-color: #ff4757;
color: #ffffff;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
min-width: 16px;
text-align: center;
}
.main-content {
flex: 1;
}
.banner-swiper {
height: 180px;
margin: 10px 15px;
border-radius: 8px;
overflow: hidden;
}
.banner-image {
width: 100%;
height: 100%;
}
.category-section {
background-color: #ffffff;
margin: 10px 15px;
border-radius: 8px;
padding: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.category-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.category-item {
width: 60px;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 15px;
}
.category-icon {
width: 40px;
height: 40px;
border-radius: 20px;
margin-bottom: 5px;
}
.category-name {
font-size: 12px;
color: #666666;
text-align: center;
}
.coupon-section {
background-color: #ffffff;
margin: 10px 15px;
border-radius: 8px;
padding: 15px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.more-btn {
color: #007aff;
font-size: 14px;
}
.coupon-scroll {
width: 100%;
}
.coupon-list {
display: flex;
flex-direction: row;
}
.coupon-item {
display: flex;
width: 280px;
height: 80px;
background: linear-gradient(135deg, #ff6b6b, #ffa726);
border-radius: 8px;
margin-right: 10px;
overflow: hidden;
}
.coupon-left {
width: 80px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.2);
}
.coupon-value {
color: #ffffff;
font-size: 18px;
font-weight: bold;
}
.coupon-condition {
color: #ffffff;
font-size: 10px;
margin-top: 2px;
}
.coupon-right {
flex: 1;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.coupon-name {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
.coupon-expire {
color: #ffffff;
font-size: 10px;
opacity: 0.8;
}
.receive-btn {
align-self: flex-end;
background-color: #ffffff;
border-radius: 12px;
padding: 4px 12px;
}
.btn-text {
color: #ff6b6b;
font-size: 12px;
}
.product-section {
background-color: #ffffff;
margin: 10px 15px;
border-radius: 8px;
padding: 15px;
}
.product-waterfall {
margin-top: 10px;
}
.product-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 10px;
}
.product-card {
width: 48%;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
}
.product-image {
width: 100%;
height: 120px;
}
.product-info {
padding: 10px;
}
.product-name {
font-size: 14px;
color: #333333;
margin-bottom: 8px;
line-height: 1.4;
}
.price-row {
display: flex;
align-items: baseline;
margin-bottom: 8px;
}
.current-price {
color: #ff4757;
font-size: 16px;
font-weight: bold;
}
.original-price {
color: #999999;
font-size: 12px;
text-decoration: line-through;
margin-left: 5px;
}
.sales-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.sales-text {
color: #999999;
font-size: 12px;
}
.add-cart-btn {
width: 24px;
height: 24px;
background-color: #007aff;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.add-text {
color: #ffffff;
font-size: 16px;
font-weight: bold;
}
.loading-container {
padding: 20px;
display: flex;
justify-content: center;
}
.loading-text {
color: #999999;
font-size: 14px;
}
.bottom-navigation {
display: flex;
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 8px 0;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 5px;
}
.nav-item.active {
color: #007aff;
}
.nav-icon {
font-size: 20px;
margin-bottom: 2px;
}
.nav-text {
font-size: 10px;
}
</style>