Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,832 @@
<!-- 消费者端首页 - 严格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>