Files
medical-mall/doc_mall/consumer/backup_pages/product-detail copy.uvue

1585 lines
44 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">
<scroll-view class="page-scroll" scroll-y="true">
<!-- 鍟嗗搧鍥剧墖杞挱 -->
<view class="product-images">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false" @change="onSwiperChange">
<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="function-section" v-if="product.usage">
<text class="function-title">鍔熻兘涓绘不</text>
<text class="function-content">{{ product.usage }}</text>
</view>
<!-- 浼樻儬鍒稿叆鍙?(鏂板) -->
<view class="coupon-entry" @click="showCouponModal" v-if="coupons.length > 0">
<view class="coupon-entry-left">
<text class="coupon-entry-label">浼樻儬</text>
<view class="coupon-tags-row">
<text class="coupon-tag" v-for="(coupon, index) in coupons.slice(0, 2)" :key="index">
{{ coupon.name }}
</text>
</view>
</view>
<text class="coupon-arrow">棰嗗埜 ></text>
</view>
<!-- 鍟嗗搧鍙傛暟 -->
<view class="params-section" @click="showParamsModal">
<text class="params-title">鍟嗗搧鍙傛暟</text>
<view class="params-summary">
<text class="params-item" v-if="product.specification">瑙勬牸: {{ product.specification }}</text>
<text class="params-item" v-if="product.expiry_date">鏈夋晥鏈? {{ product.expiry_date }}</text>
<text class="params-item" v-if="product.approval_number">鎵瑰噯鏂囧彿: {{ product.approval_number }}</text>
</view>
<text class="params-arrow">></text>
</view>
<!-- 瑙勬牸閫夋嫨 -->
<view class="spec-section" @click="showSpecModal" v-if="productSkus.length > 0">
<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"
:value="quantity.toString()"
: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 class="detail-images" v-if="product.images.length > 0">
<image v-for="(img, index) in product.images"
:key="index"
:src="img"
class="detail-image"
mode="widthFix"
@click="previewImage(index)" />
</view>
</view>
</scroll-view>
<!-- 搴曢儴鎿嶄綔鏍?-->
<view class="bottom-actions">
<view class="action-buttons">
<!-- 瀹㈡湇鎸夐挳 (鏂板) -->
<view class="action-btn" @click="contactMerchant">
<text class="action-icon">馃挰</text>
<text class="action-text">瀹㈡湇</text>
</view>
<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>
<scroll-view class="spec-list" direction="vertical">
<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>
</scroll-view>
</view>
</view>
<!-- 鍟嗗搧鍙傛暟寮圭獥 -->
<view v-if="showParams" class="params-modal" @click="hideParamsModal">
<view class="params-content" @click.stop>
<view class="params-header">
<text class="params-title">鍟嗗搧鍙傛暟</text>
<text class="close-btn" @click="hideParamsModal">脳</text>
</view>
<scroll-view class="params-list" direction="vertical">
<view class="params-item" v-if="product.specification">
<text class="params-label">瑙勬牸</text>
<text class="params-value">{{ product.specification }}</text>
</view>
<view class="params-item" v-if="product.usage">
<text class="params-label">鍔熻兘涓绘不</text>
<text class="params-value">{{ product.usage }}</text>
</view>
<view class="params-item" v-if="product.side_effects">
<text class="params-label">鍓綔鐢?/text>
<text class="params-value">{{ product.side_effects }}</text>
</view>
<view class="params-item" v-if="product.precautions">
<text class="params-label">娉ㄦ剰浜嬮」</text>
<text class="params-value">{{ product.precautions }}</text>
</view>
<view class="params-item" v-if="product.expiry_date">
<text class="params-label">鏈夋晥鏈?/text>
<text class="params-value">{{ product.expiry_date }}</text>
</view>
<view class="params-item" v-if="product.storage_conditions">
<text class="params-label">鍌ㄥ瓨鏉′欢</text>
<text class="params-value">{{ product.storage_conditions }}</text>
</view>
<view class="params-item" v-if="product.approval_number">
<text class="params-label">鎵瑰噯鏂囧彿</text>
<text class="params-value">{{ product.approval_number }}</text>
</view>
<view class="params-item" v-if="product.tags != null && product.tags.length > 0">
<text class="params-label">鏍囩</text>
<text class="params-value">{{ product.tags!.join(', ') }}</text>
</view>
</scroll-view>
</view>
</view>
<!-- 浼樻儬鍒稿脊绐?(鏂板) -->
<view v-if="showCoupons" class="popup-mask" @click="hideCouponModal">
<view class="popup-content" @click.stop>
<view class="popup-header">
<text class="popup-title">浼樻儬鍒?/text>
<text class="close-btn" @click="hideCouponModal">脳</text>
</view>
<scroll-view scroll-y="true" class="coupon-list-scroll">
<view v-for="coupon in coupons" :key="coupon.id" class="coupon-item">
<view class="coupon-left">
<text class="coupon-amount">
<text class="symbol">楼</text>{{ coupon.discount_value }}
</text>
<text class="coupon-cond">婊{ coupon.min_order_amount }}鍙敤</text>
</view>
<view class="coupon-right">
<view class="coupon-info-text">
<text class="coupon-name">{{ coupon.name }}</text>
<text class="coupon-time">{{ formatDate(coupon.start_time) }}-{{ formatDate(coupon.end_time) }}</text>
</view>
<button class="coupon-btn" @click="claimCoupon(coupon)">棰嗗彇</button>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import { ProductType, MerchantType, ProductSkuType, CouponTemplateType, FootprintItemType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.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 as number,
isFavorite: false,
showParams: false,
// 鏂板: 浼樻儬鍒哥浉鍏?
coupons: [] as Array<CouponTemplateType>,
showCoupons: false
}
},
onLoad(options: any) {
const opts = options as UTSJSONObject
const productId = (opts.getString('productId') ?? opts.getString('id')) as string
const priceStr = opts.getString('price')
const productPrice = priceStr != null ? parseFloat(priceStr) : null
const originalPriceStr = opts.getString('originalPrice')
const productOriginalPrice = originalPriceStr != null ? parseFloat(originalPriceStr) : null
let productName = opts.getString('name') as string | null
if (productName != null) {
try {
const decodedName = decodeURIComponent(productName)
productName = decodedName
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = opts.getString('image') as string | null
if (productImage != null) {
try {
const decodedImage = decodeURIComponent(productImage)
productImage = decodedImage
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId != null) {
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
name: productName,
image: productImage
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
if (productName != null) {
uni.setNavigationBarTitle({
title: productName
})
}
}
},
computed: {
displayPrice(): number {
if (this.selectedSkuId != null && this.selectedSkuId !== '') {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku != null) return sku!.price
}
return this.product.price
}
},
methods: {
saveFootprint(productId: string) {
// 璋冪敤鍚庣API璁板綍瓒宠抗
supabaseService.addFootprint(productId).then(success => {
if (success === true) {
console.log('瓒宠抗宸插悓姝ュ埌鏈嶅姟鍣?)
}
})
const footprintData = uni.getStorageSync('footprints') as string | null
let footprints: Array<FootprintItemType> = []
if (footprintData != null && footprintData !== '') {
try {
footprints = JSON.parse(footprintData) as Array<FootprintItemType>
} catch (e) {
console.error('Failed to parse footprints', e)
}
}
// 绉婚櫎宸插瓨鍦ㄧ殑鐩稿悓鍟嗗搧锛堜负浜嗗皢鍏剁Щ鍒版渶鏂颁綅缃級
const productIdStr = productId
footprints = footprints.filter(function(item: any): boolean {
const itemObj = item as UTSJSONObject
const itemId = itemObj.getString('id') ?? ''
return itemId != productIdStr
})
// 娣诲姞鍒板ご閮?
const productImage = this.product.images.length > 0 ? this.product.images[0] : '/static/default-product.png'
footprints.unshift({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price,
image: productImage,
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
viewTime: Date.now()
})
// 闄愬埗鏁伴噺锛屼緥濡傛渶杩?0鏉?
if (footprints.length > 50) {
footprints = footprints.slice(0, 50)
}
uni.setStorageSync('footprints', JSON.stringify(footprints))
},
async loadProductDetail(productId: string, options: any = {}) {
uni.showLoading({ title: '鍔犺浇涓?..' })
try {
const dbProduct = await supabaseService.getProductById(productId)
if (dbProduct != null) {
// 浣跨敤 getProductById 杩斿洖鐨?Product 瀵硅薄
this.product = {
id: dbProduct.id,
merchant_id: dbProduct.merchant_id ?? '',
category_id: dbProduct.category_id ?? '',
name: dbProduct.name,
description: dbProduct.description ?? '',
images: dbProduct.images ?? [] as string[],
price: dbProduct.price ?? dbProduct.base_price ?? 0,
original_price: dbProduct.original_price ?? dbProduct.market_price ?? 0,
stock: dbProduct.stock ?? dbProduct.total_stock ?? 0,
sales: dbProduct.sale_count ?? 0,
status: dbProduct.status ?? 1,
created_at: dbProduct.created_at ?? new Date().toISOString(),
specification: dbProduct.specification ?? null,
usage: dbProduct.usage ?? null,
side_effects: dbProduct.side_effects ?? null,
precautions: dbProduct.precautions ?? null,
expiry_date: dbProduct.expiry_date ?? null,
storage_conditions: dbProduct.storage_conditions ?? null,
approval_number: dbProduct.approval_number ?? null,
tags: [] as string[]
} as ProductType
// 瑙f瀽 tags
if (dbProduct.tags != null && dbProduct.tags != '') {
try {
const parsedTags = JSON.parse(dbProduct.tags)
if (Array.isArray(parsedTags)) {
this.product.tags = (parsedTags as any[]).map((t: any): string => t as string)
}
} catch(e) {}
}
// Handle Images - 浣跨敤 main_image_url 浣滀负鍚庡
if (this.product.images.length == 0 && dbProduct.main_image_url != null && dbProduct.main_image_url != '') {
this.product.images.push(dbProduct.main_image_url)
}
// Final fallback
if (this.product.images.length == 0) {
this.product.images.push('/static/default-product.png')
}
console.log('鍟嗗搧璇︽儏鍔犺浇鎴愬姛:', this.product.name, '搴撳瓨:', this.product.stock, '閿€閲?', this.product.sales)
} else {
throw new Error('No product found')
}
} catch (e) {
console.error('Failed to load product detail:', e)
// Fallback to options if available
this.product.id = productId
const opts = options as UTSJSONObject
const nameOpt = opts['name']
this.product.name = (nameOpt != null && nameOpt != '') ? decodeURIComponent(nameOpt as string) ?? '鏈煡鍟嗗搧' : '鏈煡鍟嗗搧'
// price 鍙兘鏄?string 鎴?number 绫诲瀷
const priceOpt = opts['price']
if (typeof priceOpt == 'number') {
this.product.price = priceOpt as number
} else if (typeof priceOpt == 'string') {
this.product.price = parseFloat(priceOpt as string)
} else {
this.product.price = 0
}
const imageOpt = opts['image']
const decodedImage = (imageOpt != null && imageOpt != '') ? decodeURIComponent(imageOpt as string) : null
this.product.images = decodedImage != null ? [decodedImage] : ['/static/default-product.png']
}
// Load Merchant and SKUs
if (this.product.merchant_id != null && this.product.merchant_id !== '') {
await this.loadMerchantInfo(this.product.merchant_id)
// 鍔犺浇浼樻儬鍒?
this.loadCoupons()
}
if (this.product.id != null && this.product.id !== '') {
this.loadProductSkus(this.product.id)
}
uni.hideLoading()
},
async loadMerchantInfo(merchantId: string) {
let realMerchantLoaded = false
if (merchantId.includes('-') || !merchantId.startsWith('merchant_')) {
try {
const shopResponse = await supabaseService.getShopByMerchantId(merchantId)
if (shopResponse != null) {
// 鐩存帴浣跨敤 Shop 瀵硅薄鐨勫睘鎬?
this.merchant = {
id: shopResponse.id,
user_id: shopResponse.merchant_id,
shop_name: shopResponse.shop_name,
shop_logo: shopResponse.shop_logo ?? '/static/default-shop.png',
shop_banner: shopResponse.shop_banner ?? '/static/default-banner.png',
shop_description: shopResponse.description ?? '',
contact_name: shopResponse.contact_name ?? '搴椾富',
contact_phone: shopResponse.contact_phone ?? '',
shop_status: 1,
rating: shopResponse.rating_avg ?? 5.0,
total_sales: shopResponse.total_sales ?? 0,
created_at: shopResponse.created_at ?? new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
console.log('搴楅摵淇℃伅鍔犺浇鎴愬姛:', this.merchant.shop_name)
}
} catch (e) {
console.error('Load shop failed', e)
}
}
if (!realMerchantLoaded) {
let charSum: number = 0
for (let i = 0; i < merchantId.length; i++) {
const charCode = merchantId.charCodeAt(i)
if (charCode != null) {
charSum += charCode
}
}
const merchantIndex = Math.abs(charSum) % 5
const shopNames = ['浼樿川濂藉簵', '鍝佺墝鐩磋惀搴?, '瀹樻柟鏃楄埌搴?, '涓撳崠搴?, '绮惧搧灏忓簵']
this.merchant = {
id: merchantId,
user_id: 'user_mock_' + merchantIndex,
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '浼樿川鏈嶅姟锛屾鍝佷繚闅?,
contact_name: '搴椾富',
contact_phone: '',
shop_status: 1,
rating: 4.8,
total_sales: 999,
created_at: '2023-01-01'
} as MerchantType
}
},
async loadProductSkus(productId: string) {
// 灏濊瘯浠庢暟鎹簱鍔犺浇SKU
try {
const skus = await supabaseService.getProductSkus(productId)
if (skus.length > 0) {
console.log('鍔犺浇鍒板晢鍝丼KU:', skus.length)
this.productSkus = []
for (let i = 0; i < skus.length; i++) {
const skuData = skus[i]
// 瑙f瀽 specifications JSON 瀛楃涓?
let specs: UTSJSONObject = {}
if (skuData.specifications != null && skuData.specifications != '') {
try {
specs = JSON.parse(skuData.specifications) as UTSJSONObject
} catch(e) {
console.error('瑙瀽SKU瑙勬牸澶辫触', e)
}
}
const sku: ProductSkuType = {
id: skuData.id,
product_id: skuData.product_id,
sku_code: skuData.sku_code,
specifications: specs,
price: skuData.price,
stock: skuData.stock ?? 0,
image_url: skuData.image_url ?? '',
status: skuData.status ?? 1
}
this.productSkus.push(sku)
}
return
}
} catch (e) {
console.error('Fetch SKUs error', e)
}
},
// 鏂板锛氬姞杞戒紭鎯犲埜
async loadCoupons() {
if (this.product.merchant_id == '') return
// Safety check for cached service definition
try {
const couponData = await supabaseService.fetchShopCoupons(this.product.merchant_id)
// 瑙f瀽浼樻儬鍒告暟鎹?
this.coupons = []
if (couponData != null && couponData.length > 0) {
for (let i = 0; i < couponData.length; i++) {
const item = couponData[i]
const couponObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const getSafeString = (key: string): string => {
const val = couponObj.get(key)
if (val == null) return ''
if (typeof val == 'string') return val
return ''
}
const getSafeNumber = (key: string): number => {
const val = couponObj.get(key)
if (val == null) return 0
if (typeof val == 'number') return val
return 0
}
const coupon: CouponTemplateType = {
id: getSafeString('id'),
name: getSafeString('name'),
description: getSafeString('description'),
coupon_type: getSafeNumber('coupon_type'),
discount_type: getSafeNumber('discount_type'),
discount_value: getSafeNumber('discount_value'),
min_order_amount: getSafeNumber('min_order_amount'),
max_discount_amount: getSafeNumber('max_discount_amount'),
total_quantity: getSafeNumber('total_quantity'),
per_user_limit: getSafeNumber('per_user_limit'),
usage_limit: getSafeNumber('usage_limit'),
merchant_id: getSafeString('merchant_id'),
category_ids: [] as string[],
product_ids: [] as string[],
user_type_limit: getSafeNumber('user_type_limit'),
start_time: getSafeString('start_time'),
end_time: getSafeString('end_time'),
status: getSafeNumber('status'),
created_at: getSafeString('created_at')
}
this.coupons.push(coupon)
}
}
} catch (e) {
console.warn('SupabaseService coupon methods not available:', e)
}
},
// 鏂板锛氳仈绯诲鏈嶏紙鍟嗗锛?
contactMerchant() {
if (supabaseService.getCurrentUserId() == '') {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
// Navigate to chat
const merchId = this.merchant.user_id ?? this.merchant.id ?? this.product.merchant_id;
uni.navigateTo({
url: `/pages/mall/consumer/chat?merchantId=${merchId}&merchantName=${this.merchant.shop_name}`
})
},
// 鏂板锛氫紭鎯犲埜寮圭獥
showCouponModal() {
this.showCoupons = true
},
hideCouponModal() {
this.showCoupons = false
},
// 鏂板锛氶鍙栦紭鎯犲埜
async claimCoupon(coupon: CouponTemplateType) {
const userId = supabaseService.getCurrentUserId()
if (userId == '') {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
uni.showLoading({ title: '棰嗗彇涓? })
let success = false
const couponId = coupon.id
try {
// @ts-ignore
success = await supabaseService.claimShopCoupon(couponId, userId!)
} catch (e) {
try {
// @ts-ignore
success = await supabaseService.claimCoupon(couponId, userId!)
} catch (e2) {
console.warn('claimCoupon method missing:', e2)
}
}
uni.hideLoading()
if (success === true) {
uni.showToast({ title: '棰嗗彇鎴愬姛', icon: 'success' })
} else {
uni.showToast({ title: '棰嗗彇澶辫触鎴栧凡棰嗗彇', icon: 'none' })
}
},
formatDate(dateStr: string): string {
if (dateStr == '') return ''
const date = new Date(dateStr)
return `${date.getFullYear()}.${date.getMonth()+1}.${date.getDate()}`
},
onSwiperChange(e: any) {
const eventObj = e as UTSJSONObject
const detail = eventObj['detail'] as UTSJSONObject
this.currentImageIndex = detail['current'] as number
},
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 != null) {
const specs = sku.specifications as UTSJSONObject
const keys = ['瑙勬牸', '棰滆壊', '灏虹爜', '瀹归噺', '鐗堟湰', '鍨嬪彿']
const result: string[] = []
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const val = specs.get(key)
// 澧炲姞鏇翠弗鏍肩殑鍒ゆ柇锛岄槻姝?null 鎴?undefined
if (val != null && val !== '') {
result.push(`${val}`)
}
}
if (result.length > 0) {
return result.join(' ')
}
// 鍏煎澶勭悊锛氬鏋滄病鏈夐璁剧殑 key锛屽皾璇曠洿鎺ュ彇鍑虹涓€涓€?
// 鍦?UTS 涓幏鍙栧璞$殑鎵€鏈?key 姣旇緝澶嶆潅锛岃繖閲屽仛涓€涓畝鍗曠殑鍏滃簳
const specStr = JSON.stringify(specs)
if (specStr.includes(':')) {
// 杩欐槸涓€涓畝鍖栫殑鎻愬彇閫昏緫锛屼粠 {"key":"value"} 涓彁鍙?value
// 浠呬綔涓洪璁?key 鍖归厤涓嶅埌鏃剁殑鍏滃簳
try {
const parts = specStr.split(':')
if (parts.length > 1) {
let val = parts[1].replace(/["'}]/g, '').trim()
if (val !== '') return val
}
} catch (e) {}
}
return sku.sku_code != null && sku.sku_code !== '' ? sku.sku_code : '榛樿瑙勬牸'
}
return sku.sku_code != null && sku.sku_code !== '' ? sku.sku_code : '榛樿瑙勬牸'
},
async addToCart() {
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
uni.showToast({
title: '璇烽€夋嫨瑙勬牸',
icon: 'none'
})
return
}
uni.showLoading({ title: '娣诲姞涓?..' })
try {
const success = await supabaseService.addToCart(
this.product.id,
this.quantity,
this.selectedSkuId,
this.product.merchant_id
)
uni.hideLoading()
if (success === true) {
uni.showToast({ title: '宸叉坊鍔犲埌璐墿杞?, icon: 'success' })
} else {
console.error('娣诲姞璐墿杞﹁繑鍥炲け璐?)
uni.showToast({ title: '娣诲姞澶辫触锛岃鐧诲綍閲嶈瘯', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('娣诲姞璐墿杞﹀紓甯?, e)
uni.showToast({ title: '娣诲姞寮傚父', icon: 'none' })
}
},
buyNow() {
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
uni.showToast({
title: '璇烽€夋嫨瑙勬牸',
icon: 'none'
})
return
}
const sku = (this.selectedSkuId != null && this.selectedSkuId !== '') ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
const selectedItem = {
id: this.selectedSkuId,
product_id: this.product.id,
sku_id: this.selectedSkuId,
product_name: this.product.name,
product_image: (sku != null && sku.image_url != null) ? sku!.image_url : this.product.images[0],
sku_specifications: sku != null ? sku!.specifications : {},
price: parseFloat((sku != null ? sku!.price : this.product.price).toString()).toFixed(2) as string,
quantity: this.quantity as number,
shop_id: this.merchant.id,
shop_name: this.merchant.shop_name,
merchant_id: this.merchant.user_id ?? this.product.merchant_id
}
uni.setStorageSync('checkout_type', 'buy_now')
uni.setStorageSync('checkout_items', JSON.stringify([selectedItem]))
uni.navigateTo({
url: '/pages/mall/consumer/checkout'
})
},
checkFavoriteStatus(id: string) {
this.checkFavorite(id)
},
async checkFavorite(id: string) {
const isFav = await supabaseService.checkFavorite(id)
this.isFavorite = isFav
},
async toggleFavorite() {
if (this.product.id == '') return
uni.showLoading({ title: '澶勭悊涓? })
try {
const wasFavorite = this.isFavorite
const isNowFavorite = await supabaseService.toggleFavorite(this.product.id)
uni.hideLoading()
if (isNowFavorite !== wasFavorite) {
this.isFavorite = isNowFavorite
uni.showToast({
title: isNowFavorite ? '鏀惰棌鎴愬姛' : '宸插彇娑堟敹钘?,
icon: 'success'
})
} else {
uni.showToast({ title: '鎿嶄綔澶辫触', icon: 'none' })
this.checkFavoriteStatus(this.product.id)
}
} catch (e) {
uni.hideLoading()
console.error('Toggle favorite failed', e)
uni.showToast({ title: '鎿嶄綔寮傚父', icon: 'none' })
}
},
goToHome() {
uni.switchTab({ url: '/pages/mall/consumer/home' })
},
goToShop() {
const merchantId = this.merchant.id ?? this.product.merchant_id ?? ''
if (merchantId != '') {
console.log('杩涘簵鐐瑰嚮锛宮erchantId:', merchantId)
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${merchantId}`
})
} else {
uni.showToast({
title: '搴楅摵淇℃伅鍔犺浇涓?,
icon: 'none'
})
}
},
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 = this.quantity
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() {
if (this.selectedSkuId != null && this.selectedSkuId !== '') {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku != null) return sku!.stock
}
return this.product.stock
},
getAvailableStock() {
return this.getMaxQuantity()
},
previewImage(index: number) {
uni.previewImage({
current: index,
urls: this.product.images
})
},
showParamsModal() {
this.showParams = true
},
hideParamsModal() {
this.showParams = false
}
}
}
</script>
<style>
.product-detail-page {
background-color: #f5f5f5;
flex: 1;
display: flex;
flex-direction: column;
}
.page-scroll {
flex: 1;
height: 0;
width: 100%;
}
.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: #ff5000;
margin-right: 20rpx;
}
.original-price {
font-size: 28rpx;
color: #999;
text-decoration-line: 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;
}
/* Coupon Entry Styles */
.coupon-entry {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.coupon-entry-left {
display: flex;
align-items: center;
flex: 1;
}
.coupon-entry-label {
font-size: 30rpx;
color: #333;
width: 120rpx;
font-weight: bold;
}
.coupon-tags-row {
flex: 1;
display: flex;
flex-direction: row;
}
.coupon-tag {
font-size: 20rpx;
color: #ff5000;
border: 1px solid #ff5000;
padding: 2rpx 10rpx;
border-radius: 4rpx;
margin-right: 15rpx;
}
.coupon-arrow {
font-size: 26rpx;
color: #999;
}
/* Modal Popup Styles */
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: flex-end;
flex-direction: column;
z-index: 1000;
}
.popup-content {
background-color: #fff;
width: 100%;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
font-size: 48rpx;
color: #999;
}
.coupon-list-scroll {
flex: 1;
}
.coupon-item {
display: flex;
background-color: #fff5f5;
border-radius: 10rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.coupon-left {
width: 180rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-right: 1px dashed #ffccc7;
color: #ff5000;
}
.coupon-amount {
font-size: 40rpx;
font-weight: bold;
}
.symbol {
font-size: 24rpx;
}
.coupon-cond {
font-size: 22rpx;
margin-top: 5rpx;
}
.coupon-right {
flex: 1;
padding-left: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.coupon-info-text {
display: flex;
flex-direction: column;
}
.coupon-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.coupon-time {
font-size: 22rpx;
color: #999;
}
.coupon-btn {
background-color: #ff5000;
color: #fff;
font-size: 24rpx;
padding: 0 24rpx;
height: 50rpx;
line-height: 50rpx;
border-radius: 25rpx;
margin: 0;
}
.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;
padding-bottom: 140rpx;
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: #ff5000;
color: #fff;
}
.buy-btn {
background-color: #ff5000;
color: #fff;
}
.spec-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: flex-end; /* UVUE 鎺ㄨ崘鐢?flex 甯冨眬瀵归綈 */
flex-direction: column;
z-index: 999;
}
.spec-content {
background-color: #fff;
width: 100%;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
}
.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;
}
.spec-list {
flex: 1;
}
.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: #ff5000;
margin-right: 20rpx;
}
.spec-stock {
font-size: 24rpx;
color: #666;
width: 100rpx;
text-align: right;
}
/* 鍔熻兘涓绘不鏍峰紡 */
.function-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.function-title {
font-size: 30rpx;
color: #333;
font-weight: bold;
margin-bottom: 15rpx;
}
.function-content {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 鍟嗗搧鍙傛暟鏍峰紡 */
.params-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.params-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
flex-shrink: 0;
}
.params-summary {
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.params-item {
font-size: 26rpx;
color: #666;
line-height: 1.5;
margin-right: 20rpx;
margin-bottom: 5rpx;
white-space: nowrap;
}
.params-arrow {
font-size: 28rpx;
color: #999;
flex-shrink: 0;
margin-left: 10rpx;
}
/* 鍟嗗搧鍙傛暟寮圭獥鏍峰紡 */
.params-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: flex-end; /* UVUE 鎺ㄨ崘鐢?flex 甯冨眬瀵归綈 */
flex-direction: column;
z-index: 1000;
}
.params-content {
background-color: #fff;
width: 100%;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
display: flex;
flex-direction: column;
max-height: 1000rpx;
}
.params-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.params-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
width: auto;
}
.params-list {
flex: 1;
}
.params-item {
display: flex;
align-items: flex-start;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.params-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
width: 150rpx;
flex-shrink: 0;
}
.params-value {
flex: 1;
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 鍟嗗搧璇︽儏鍥剧墖鏍峰紡 */
.detail-images {
margin-top: 30rpx;
}
.detail-image {
width: 100%;
margin-bottom: 20rpx;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
/* 鐢佃剳绔€傞厤 */
@media (min-width: 768px) {
.params-section {
padding: 20rpx 30rpx;
}
.params-summary {
flex-wrap: nowrap;
justify-content: space-between;
}
.params-item {
flex: 1;
margin-right: 0;
text-align: center;
padding: 0 10rpx;
}
.params-arrow {
margin-left: 20rpx;
}
}
</style>