consumer模块完成度85%,连接服务器supabase,新建相关表

This commit is contained in:
2026-01-29 17:28:47 +08:00
parent a4fa00c935
commit ab038ec029
15 changed files with 4475 additions and 4793 deletions

View File

@@ -0,0 +1,979 @@
<!-- 消费者端 - 商品详情页 -->
<template>
<view class="product-detail-page">
<!-- 商品图片轮播 -->
<view class="product-images">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false">
<swiper-item v-for="(image, index) in product.images" :key="index">
<image :src="image" class="product-image" mode="aspectFit" />
</swiper-item>
</swiper>
<view class="image-indicator">{{ currentImageIndex + 1 }} / {{ product.images.length }}</view>
</view>
<!-- 商品基本信息 -->
<view class="product-info">
<view class="price-section">
<text class="current-price">¥{{ product.price }}</text>
<text v-if="product.original_price" class="original-price">¥{{ product.original_price }}</text>
</view>
<text class="product-name">{{ product.name }}</text>
<text class="sales-info">已售{{ product.sales }}件 · 库存{{ product.stock }}件</text>
</view>
<!-- 店铺信息 -->
<view class="shop-info" @click="goToShop">
<image :src="merchant.shop_logo || '/static/default-shop.png'" class="shop-logo" />
<view class="shop-details">
<text class="shop-name" @click.stop="goToShop">{{ merchant.shop_name }}</text>
<view class="shop-stats-row">
<text class="rating-text">评分: {{ merchant.rating.toFixed(1) }}</text>
<text class="sales-text">销量: {{ merchant.total_sales }}</text>
</view>
</view>
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal">
<text class="spec-title">规格</text>
<text class="spec-selected">{{ selectedSpec || '请选择规格' }}</text>
<text class="spec-arrow">></text>
</view>
<!-- 数量选择 -->
<view class="quantity-section">
<text class="quantity-title">数量</text>
<view class="quantity-selector">
<view class="quantity-btn minus" @click="decreaseQuantity">
<text class="quantity-btn-text">-</text>
</view>
<input class="quantity-input"
type="number"
v-model="quantity"
:min="1"
:max="getMaxQuantity()"
@input="validateQuantity" />
<view class="quantity-btn plus" @click="increaseQuantity">
<text class="quantity-btn-text">+</text>
</view>
</view>
<text class="quantity-stock">库存{{ getAvailableStock() }}件</text>
</view>
<!-- 商品详情 -->
<view class="product-description">
<view class="section-title">商品详情</view>
<text class="description-text">{{ product.description || '暂无详细描述' }}</text>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-buttons">
<view class="action-btn" @click="goToCart">
<text class="action-icon">🛒</text>
<text class="action-text">购物车</text>
</view>
<view class="action-btn" @click="toggleFavorite">
<text class="action-icon">{{ isFavorite ? '❤️' : '🤍' }}</text>
<text class="action-text">{{ isFavorite ? '已收藏' : '收藏' }}</text>
</view>
</view>
<view class="btn-group">
<button class="cart-btn" @click="addToCart">加入购物车</button>
<button class="buy-btn" @click="buyNow">立即购买</button>
</view>
</view>
<!-- 规格选择弹窗 -->
<view v-if="showSpec" class="spec-modal" @click="hideSpecModal">
<view class="spec-content" @click.stop>
<view class="spec-header">
<text class="spec-title">选择规格</text>
<text class="close-btn" @click="hideSpecModal">×</text>
</view>
<view class="spec-list">
<view v-for="sku in productSkus" :key="sku.id"
class="spec-item"
:class="{ active: selectedSkuId === sku.id }"
@click="selectSku(sku)">
<text class="spec-name">{{ getSkuSpecText(sku) }}</text>
<text class="spec-price">¥{{ sku.price }}</text>
<text class="spec-stock">库存{{ sku.stock }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
export default {
data() {
return {
product: {
id: '',
merchant_id: '',
category_id: '',
name: '',
description: '',
images: [] as Array<string>,
price: 0,
original_price: 0,
stock: 0,
sales: 0,
status: 0,
created_at: ''
} as ProductType,
merchant: {
id: '',
user_id: '',
shop_name: '',
shop_logo: '',
shop_banner: '',
shop_description: '',
contact_name: '',
contact_phone: '',
shop_status: 0,
rating: 0,
total_sales: 0,
created_at: ''
} as MerchantType,
productSkus: [] as Array<ProductSkuType>,
currentImageIndex: 0,
showSpec: false,
selectedSkuId: '',
selectedSpec: '',
quantity: 1,
isFavorite: false
}
},
onLoad(options: any) {
const productId = options.productId as string || options.id as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
// 处理商品名称:如果是编码的则解码,否则直接使用
let productName = options.name as string
if (productName) {
try {
// 尝试解码如果失败不是有效的URI组件则使用原值
productName = decodeURIComponent(productName)
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = options.image as string
if (productImage) {
try {
productImage = decodeURIComponent(productImage)
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId) {
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
name: productName,
image: productImage
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
// 设置导航栏标题为商品名称
if (productName) {
uni.setNavigationBarTitle({
title: productName
})
}
}
},
computed: {
displayPrice(): number {
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.price
}
return this.product.price
}
},
methods: {
saveFootprint(productId: string) {
const footprintData = uni.getStorageSync('footprints')
let footprints: any[] = []
if (footprintData) {
try {
footprints = JSON.parse(footprintData as string) as any[]
} catch (e) {
console.error('Failed to parse footprints', e)
}
}
// 移除已存在的相同商品(为了将其移到最新位置)
footprints = footprints.filter(item => item.id !== productId)
// 添加到头部
footprints.unshift({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 添加原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
viewTime: Date.now()
})
// 限制数量例如最近50条
if (footprints.length > 50) {
footprints = footprints.slice(0, 50)
}
uni.setStorageSync('footprints', JSON.stringify(footprints))
},
loadProductDetail(productId: string, options: any = {}) {
// 根据商品ID生成一个基础价格如果没有传入价格
const generatePriceFromId = (id: string): number => {
// 简单哈希函数将字符串转换为一个在50-500之间的价格
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0 // 转换为32位整数
}
// 将哈希值映射到50-500之间
const price = 50 + Math.abs(hash % 450)
// 保留两位小数
return parseFloat(price.toFixed(2))
}
// 优先使用传入的参数否则根据商品ID生成价格
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
// 优先使用传入的商品名称否则根据商品ID生成名称
const productName = options.name ? decodeURIComponent(options.name) : (() => {
// 如果options.name未传入使用默认的商品名称生成逻辑
const productNames = [
'高品质运动休闲鞋',
'时尚简约双肩背包',
'多功能智能手环',
'便携式蓝牙音箱',
'全自动雨伞',
'抗菌防螨床上四件套',
'不锈钢保温杯',
'无线充电器',
'高清行车记录仪',
'智能体脂秤'
]
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
// 优先使用传入的图片,否则使用默认图片
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
productImage,
'/static/product2.jpg',
'/static/product3.jpg'
],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
this.loadProductSkus(productId)
},
loadProductSkus(productId: string) {
// 模拟加载商品SKU数据
const basePrice = this.product.price
// 使用 productId 作为前缀生成唯一的 SKU ID防止不同商品的 SKU ID 冲突
this.productSkus = [
{
id: `${productId}_sku_001`,
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: basePrice,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
},
{
id: `${productId}_sku_002`,
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: parseFloat((basePrice * 1.1).toFixed(2)),
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
}
]
},
showSpecModal() {
this.showSpec = true
},
hideSpecModal() {
this.showSpec = false
},
selectSku(sku: ProductSkuType) {
this.selectedSkuId = sku.id
this.selectedSpec = this.getSkuSpecText(sku)
this.hideSpecModal()
},
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications) {
const specs: any = sku.specifications
return Object.keys(specs).map(key => `${key}: ${specs[key]}`).join(', ')
}
return sku.sku_code
},
addToCart() {
if (!this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
})
return
}
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在 (同一SKU)
const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
if (existingItem) {
existingItem.quantity += this.quantity
} else {
// 查找SKU信息
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
// 添加新商品
cartItems.push({
id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
productId: this.product.id,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
name: this.product.name,
price: sku ? sku.price : this.product.price,
image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
spec: this.selectedSpec,
quantity: this.quantity,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
// 模拟添加到购物车
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
},
buyNow() {
if (!this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
})
return
}
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
console.log('SKU价格:', sku ? sku.price : '无SKU')
console.log('商品价格:', this.product.price)
console.log('选择的价格:', (sku ? sku.price : this.product.price))
console.log('数量:', this.quantity)
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
sku_specifications: sku ? sku.specifications : {},
price: Number(parseFloat((sku ? sku.price : this.product.price).toString()).toFixed(2)),
quantity: Number(this.quantity)
}
// 调试:打印最终传递的数据
console.log('立即购买 - 传递的商品数据:', selectedItem)
// 使用Storage传递数据避免EventChannel可能的问题
uni.setStorageSync('checkout_type', 'buy_now')
uni.setStorageSync('checkout_items', JSON.stringify([selectedItem]))
// 跳转到订单确认页
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
success: (res) => {
res.eventChannel.emit('acceptData', {
selectedItems: [selectedItem]
})
}
})
},
goToShop() {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
})
},
checkFavoriteStatus(id: string) {
const storedFavorites = uni.getStorageSync('favorites')
if (storedFavorites) {
try {
const favorites = JSON.parse(storedFavorites as string) as any[]
this.isFavorite = favorites.some(item => item.id === id)
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
},
toggleFavorite() {
const storedFavorites = uni.getStorageSync('favorites')
let favorites: any[] = []
if (storedFavorites) {
try {
favorites = JSON.parse(storedFavorites as string) as any[]
} catch (e) {
console.error('Failed to parse favorites', e)
}
}
if (this.isFavorite) {
// 取消收藏
favorites = favorites.filter(item => item.id !== this.product.id)
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
} else {
// 添加收藏
favorites.push({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price, // 保存原价
image: this.product.images[0],
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name
})
uni.showToast({
title: '收藏成功',
icon: 'success'
})
}
uni.setStorageSync('favorites', JSON.stringify(favorites))
this.isFavorite = !this.isFavorite
},
goToHome() {
uni.switchTab({
url: '/pages/mall/consumer/home'
})
},
goToCart() {
uni.switchTab({
url: '/pages/mall/consumer/cart'
})
},
// 数量选择相关方法
decreaseQuantity() {
if (this.quantity > 1) {
this.quantity--
}
},
increaseQuantity() {
const maxQuantity = this.getMaxQuantity()
if (this.quantity < maxQuantity) {
this.quantity++
} else {
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
},
validateQuantity() {
// 确保数量是数字
let num = parseInt(this.quantity)
if (isNaN(num)) {
num = 1
}
// 限制在1和最大库存之间
const maxQuantity = this.getMaxQuantity()
if (num < 1) {
num = 1
} else if (num > maxQuantity) {
num = maxQuantity
uni.showToast({
title: `最多只能购买${maxQuantity}件`,
icon: 'none'
})
}
this.quantity = num
},
getMaxQuantity() {
// 如果有选择SKU使用SKU的库存否则使用商品总库存
if (this.selectedSkuId) {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.stock
}
return this.product.stock
},
getAvailableStock() {
return this.getMaxQuantity()
}
}
}
</script>
<style>
.product-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
}
.product-images {
position: relative;
height: 750rpx;
background-color: #fff;
}
.image-swiper {
width: 100%;
height: 100%;
}
.product-image {
width: 100%;
height: 100%;
}
.image-indicator {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 10rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.product-info {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.price-section {
margin-bottom: 20rpx;
}
.current-price {
font-size: 48rpx;
font-weight: bold;
color: #ff4444;
margin-right: 20rpx;
}
.original-price {
font-size: 28rpx;
color: #999;
text-decoration: line-through;
}
.product-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
margin-bottom: 15rpx;
}
.sales-info {
font-size: 26rpx;
color: #666;
}
.shop-info {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
flex-direction: row; /* 显式横向排列 */
align-items: center;
}
.shop-logo {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.shop-details {
flex: 1;
display: flex;
flex-direction: column; /* 内部信息保持纵向,或者根据需要改为横向 */
justify-content: center;
}
.shop-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.shop-stats-row {
display: flex;
flex-direction: row; /* 显式横向排列 */
align-items: center;
}
.rating-text, .sales-text {
font-size: 24rpx;
color: #666;
margin-right: 30rpx;
}
.enter-shop {
font-size: 26rpx;
color: #666;
}
.spec-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.spec-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
.spec-selected {
flex: 1;
font-size: 28rpx;
color: #666;
}
.spec-arrow {
font-size: 28rpx;
color: #999;
}
.quantity-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.quantity-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
}
.quantity-selector {
display: flex;
align-items: center;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
overflow: hidden;
}
.quantity-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
.quantity-btn.minus {
border-right: 1rpx solid #e5e5e5;
}
.quantity-btn.plus {
border-left: 1rpx solid #e5e5e5;
}
.quantity-btn-text {
font-size: 28rpx;
color: #333;
}
.quantity-input {
width: 80rpx;
height: 60rpx;
text-align: center;
font-size: 28rpx;
color: #333;
border: none;
background-color: #fff;
}
.quantity-stock {
font-size: 24rpx;
color: #666;
}
.product-description {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.description-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 10rpx 20rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: row; /* 显式设置横向排列 */
align-items: center;
justify-content: space-between;
}
.action-buttons {
display: flex;
flex-direction: row; /* 显式设置横向排列 */
align-items: center;
margin-right: 20rpx;
}
.action-btn {
display: flex;
flex-direction: column; /* 图标文字保持纵向 */
align-items: center;
justify-content: center;
margin-right: 20rpx;
min-width: 80rpx;
}
.action-icon {
font-size: 40rpx;
margin-bottom: 4rpx;
}
.action-text {
font-size: 20rpx;
color: #666;
}
.btn-group {
flex: 1;
display: flex;
flex-direction: row; /* 显式设置横向排列 */
align-items: center;
}
.cart-btn, .buy-btn {
flex: 1;
height: 72rpx;
line-height: 72rpx;
border-radius: 36rpx;
font-size: 26rpx;
border: none;
margin: 0 10rpx;
}
.cart-btn {
background-color: #ffa726;
color: #fff;
}
.buy-btn {
background-color: #ff4444;
color: #fff;
}
.spec-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 999;
}
.spec-content {
background-color: #fff;
width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
}
.spec-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.spec-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
font-size: 48rpx;
color: #999;
}
.spec-list {
max-height: 60vh;
overflow-y: auto;
}
.spec-item {
display: flex;
align-items: center;
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.spec-item.active {
background-color: #fff3e0;
}
.spec-name {
flex: 1;
font-size: 28rpx;
color: #333;
}
.spec-price {
font-size: 26rpx;
color: #ff4444;
margin-right: 20rpx;
}
.spec-stock {
font-size: 24rpx;
color: #666;
width: 100rpx;
text-align: right;
}
</style>