Files
medical-mall/pages/mall/consumer/product-detail - 副本.uvue

980 lines
25 KiB
Plaintext
Raw 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.
<!-- 消费者端 - 商品详情页 -->
<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>