consumer模块完成度95%,能编译在安卓端运行,在解决数据获取和页面布局问题

This commit is contained in:
cyh666666
2026-02-27 08:20:43 +08:00
parent e606c597ca
commit b9acce6c35
1554 changed files with 23471 additions and 8551 deletions

View File

@@ -81,7 +81,7 @@
</view>
<input class="quantity-input"
type="number"
v-model="quantity"
:value="quantity.toString()"
:min="1"
:max="getMaxQuantity()"
@input="validateQuantity" />
@@ -97,7 +97,7 @@
<view class="section-title">商品详情</view>
<text class="description-text">{{ product.description ?? '暂无详细描述' }}</text>
<!-- 商品详情图片 -->
<view class="detail-images" v-if="product.images && product.images.length > 0">
<view class="detail-images" v-if="product.images.length > 0">
<image v-for="(img, index) in product.images"
:key="index"
:src="img"
@@ -187,9 +187,9 @@
<text class="params-label">批准文号</text>
<text class="params-value">{{ product.approval_number }}</text>
</view>
<view class="params-item" v-if="product.tags && product.tags.length > 0">
<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>
<text class="params-value">{{ product.tags!.join(', ') }}</text>
</view>
</scroll-view>
</view>
@@ -226,7 +226,7 @@
</template>
<script>
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
import { ProductType, MerchantType, ProductSkuType, CouponTemplateType, FootprintItemType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
export default {
@@ -269,36 +269,39 @@ export default {
isFavorite: false,
showParams: false,
// 新增: 优惠券相关
coupons: [] as any[],
coupons: [] as Array<CouponTemplateType>,
showCoupons: false
}
},
onLoad(options: any) {
const productId = (options['productId'] ?? options['id']) as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
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 = options.name as string
if (productName) {
let productName = opts.getString('name') as string | null
if (productName != null) {
try {
// 尝试解码如果失败不是有效的URI组件则使用原值
productName = decodeURIComponent(productName)
const decodedName = decodeURIComponent(productName)
productName = decodedName
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = options.image as string
if (productImage) {
let productImage = opts.getString('image') as string | null
if (productImage != null) {
try {
productImage = decodeURIComponent(productImage)
const decodedImage = decodeURIComponent(productImage)
productImage = decodedImage
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId) {
if (productId != null) {
this.loadProductDetail(productId, {
price: productPrice,
originalPrice: productOriginalPrice,
@@ -308,8 +311,7 @@ export default {
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
// 设置导航栏标题为商品名称
if (productName) {
if (productName != null) {
uni.setNavigationBarTitle({
title: productName
})
@@ -318,9 +320,9 @@ export default {
},
computed: {
displayPrice(): number {
if (this.selectedSkuId) {
if (this.selectedSkuId != null && this.selectedSkuId !== '') {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.price
if (sku != null) return sku!.price
}
return this.product.price
}
@@ -329,17 +331,17 @@ export default {
saveFootprint(productId: string) {
// 调用后端API记录足迹
supabaseService.addFootprint(productId).then(success => {
if (success) {
if (success === true) {
console.log('足迹已同步到服务器')
}
})
const footprintData = uni.getStorageSync('footprints')
let footprints: any[] = []
const footprintData = uni.getStorageSync('footprints') as string | null
let footprints: Array<FootprintItemType> = []
if (footprintData) {
if (footprintData != null && footprintData !== '') {
try {
footprints = JSON.parse(footprintData as string) as any[]
footprints = JSON.parse(footprintData) as Array<FootprintItemType>
} catch (e) {
console.error('Failed to parse footprints', e)
}
@@ -372,55 +374,59 @@ export default {
uni.showLoading({ title: '加载中...' })
try {
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any | null = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
dbProduct = dbProductResponse
let dbProduct: UTSJSONObject | null = null
if (Array.isArray(dbProductResponse)) {
const arr = dbProductResponse as any[]
if (arr.length > 0) {
dbProduct = arr[0] as UTSJSONObject
}
} else if (dbProductResponse != null) {
dbProduct = dbProductResponse as UTSJSONObject
}
if (dbProduct) {
if (dbProduct != null) {
// Map DB product to local product
this.product = {
id: dbProduct.id,
merchant_id: dbProduct.merchant_id ?? dbProduct.shop_id ?? '',
category_id: dbProduct.category_id ?? '',
name: dbProduct.name,
description: dbProduct.description ?? '',
id: dbProduct['id'] as string,
merchant_id: (dbProduct['merchant_id'] ?? dbProduct['shop_id'] ?? '') as string,
category_id: (dbProduct['category_id'] ?? '') as string,
name: dbProduct['name'] as string,
description: (dbProduct['description'] ?? '') as string,
images: [] as string[],
price: dbProduct.base_price ?? dbProduct.price ?? 0,
original_price: dbProduct.market_price ?? dbProduct.original_price ?? 0,
stock: dbProduct.available_stock ?? dbProduct.total_stock ?? dbProduct.stock ?? 0,
sales: dbProduct.sale_count ?? dbProduct.sales ?? 0,
status: dbProduct.status !== undefined ? dbProduct.status : 1,
created_at: dbProduct.created_at ?? new Date().toISOString(),
price: (dbProduct['base_price'] ?? dbProduct['price'] ?? 0) as number,
original_price: (dbProduct['market_price'] ?? dbProduct['original_price'] ?? 0) as number,
stock: (dbProduct['available_stock'] ?? dbProduct['total_stock'] ?? dbProduct['stock'] ?? 0) as number,
sales: (dbProduct['sale_count'] ?? dbProduct['sales'] ?? 0) as number,
status: dbProduct['status'] != null ? dbProduct['status'] as number : 1,
created_at: (dbProduct['created_at'] ?? new Date().toISOString()) as string,
// Attributes
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,
specification: dbProduct['specification'] as string | null,
usage: dbProduct['usage'] as string | null,
side_effects: dbProduct['side_effects'] as string | null,
precautions: dbProduct['precautions'] as string | null,
expiry_date: dbProduct['expiry_date'] as string | null,
storage_conditions: dbProduct['storage_conditions'] as string | null,
approval_number: dbProduct['approval_number'] as string | null,
tags: [] as string[]
} as ProductType
// Handle Images
if (dbProduct.image_urls) {
if (dbProduct['image_urls'] != null) {
try {
const parsed = typeof dbProduct.image_urls === 'string' ? JSON.parse(dbProduct.image_urls) : dbProduct.image_urls
const imageUrls = dbProduct['image_urls']
const parsed = typeof imageUrls === 'string' ? JSON.parse(imageUrls) : imageUrls
if (Array.isArray(parsed)) {
this.product.images = parsed.map((i: any) => String(i))
this.product.images = (parsed as any[]).map((i: any): string => i as string)
}
} catch (e) { console.error('Error parsing image_urls', e) }
}
// Fallback to main_image_url if no images found
if (this.product.images.length === 0 && dbProduct.main_image_url) {
this.product.images.push(dbProduct.main_image_url)
if (this.product.images.length === 0 && dbProduct['main_image_url'] != null) {
this.product.images.push(dbProduct['main_image_url'] as string)
}
// Fallback to 'image' field (legacy)
if (this.product.images.length === 0 && dbProduct.image) {
this.product.images.push(dbProduct.image)
if (this.product.images.length === 0 && dbProduct['image'] != null) {
this.product.images.push(dbProduct['image'] as string)
}
// Final fallback
if (this.product.images.length === 0) {
@@ -428,23 +434,25 @@ export default {
}
// Handle Tags
if (dbProduct.tags) {
if (dbProduct['tags'] != null) {
try {
const parsedTags = typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags
const tagsData = dbProduct['tags']
const parsedTags = typeof tagsData === 'string' ? JSON.parse(tagsData) : tagsData
if (Array.isArray(parsedTags)) {
this.product.tags = parsedTags.map((t: any) => String(t))
this.product.tags = (parsedTags as any[]).map((t: any): string => t as string)
}
} catch (e) {}
}
// Handle JSON attributes if present
if (dbProduct.attributes && typeof dbProduct.attributes === 'string') {
const attributes = dbProduct['attributes']
if (attributes != null && typeof attributes === 'string') {
try {
const attrs = JSON.parse(dbProduct.attributes)
if (attrs) {
const attrs = JSON.parse(attributes) as UTSJSONObject | null
if (attrs != null) {
// Merge attributes into product if they match keys
if (attrs.specification) this.product.specification = attrs.specification
if (attrs.usage) this.product.usage = attrs.usage
if (attrs['specification'] != null) this.product.specification = attrs['specification'] as string
if (attrs['usage'] != null) this.product.usage = attrs['usage'] as string
// ... augment as needed
}
} catch(e) {}
@@ -456,18 +464,23 @@ export default {
console.error('Failed to load product detail:', e)
// Fallback to options if available
this.product.id = productId
this.product.name = options.name ? decodeURIComponent(options.name) : '未知商品'
this.product.price = options.price ? parseFloat(options.price) : 0
this.product.images = options.image ? [decodeURIComponent(options.image)] : ['/static/default-product.png']
const opts = options as UTSJSONObject
const nameOpt = opts['name'] as string | null
this.product.name = (nameOpt != null && nameOpt !== '') ? decodeURIComponent(nameOpt) ?? '未知商品' : '未知商品'
const priceOpt = opts['price'] as string | null
this.product.price = (priceOpt != null && priceOpt !== '') ? parseFloat(priceOpt) : 0
const imageOpt = opts['image'] as string | null
const decodedImage = (imageOpt != null && imageOpt !== '') ? decodeURIComponent(imageOpt) : null
this.product.images = decodedImage != null ? [decodedImage] : ['/static/default-product.png']
}
// Load Merchant and SKUs
if (this.product.merchant_id) {
if (this.product.merchant_id != null && this.product.merchant_id !== '') {
await this.loadMerchantInfo(this.product.merchant_id)
// 加载优惠券
this.loadCoupons()
}
if (this.product.id) {
if (this.product.id != null && this.product.id !== '') {
this.loadProductSkus(this.product.id)
}
@@ -478,21 +491,22 @@ export default {
let realMerchantLoaded = false
if (merchantId.includes('-') || !merchantId.startsWith('merchant_')) {
try {
const shop = await supabaseService.getShopByMerchantId(merchantId)
if (shop) {
const shopResponse = await supabaseService.getShopByMerchantId(merchantId)
if (shopResponse != null) {
const shop = shopResponse as UTSJSONObject
this.merchant = {
id: shop.id,
user_id: shop.merchant_id,
shop_name: shop.shop_name,
shop_logo: shop.shop_logo ?? '/static/default-shop.png',
shop_banner: shop.shop_banner ?? '/static/default-banner.png',
shop_description: shop.description ?? '',
contact_name: shop.contact_name ?? '店主',
contact_phone: shop.contact_phone ?? '',
id: shop['id'] as string,
user_id: shop['merchant_id'] as string,
shop_name: shop['shop_name'] as string,
shop_logo: (shop['shop_logo'] ?? '/static/default-shop.png') as string,
shop_banner: (shop['shop_banner'] ?? '/static/default-banner.png') as string,
shop_description: (shop['description'] ?? '') as string,
contact_name: (shop['contact_name'] ?? '店主') as string,
contact_phone: (shop['contact_phone'] ?? '') as string,
shop_status: 1,
rating: shop.rating_avg ?? 5.0,
total_sales: shop.total_sales ?? 0,
created_at: shop.created_at ?? new Date().toISOString()
rating: (shop['rating_avg'] ?? 5.0) as number,
total_sales: (shop['total_sales'] ?? 0) as number,
created_at: (shop['created_at'] ?? new Date().toISOString()) as string
} as MerchantType
realMerchantLoaded = true
}
@@ -502,7 +516,14 @@ export default {
}
if (!realMerchantLoaded) {
const merchantIndex = Math.abs(merchantId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
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 = {
@@ -528,29 +549,31 @@ export default {
const skus = await supabaseService.getProductSkus(productId)
if (skus.length > 0) {
console.log('加载到商品SKU:', skus.length)
this.productSkus = skus.map((sku): ProductSkuType => {
let specs: UTSJSONObject = {}
if (sku.specifications) {
try {
if (typeof sku.specifications === 'string') {
specs = JSON.parse(sku.specifications) as UTSJSONObject
} else {
// 假设已经是对象
specs = sku.specifications as unknown as UTSJSONObject
this.productSkus = skus.map((skuData): ProductSkuType => {
const sku = skuData as UTSJSONObject
let specs: UTSJSONObject = {}
const specsData = sku['specifications']
if (specsData != null) {
try {
if (typeof specsData === 'string') {
specs = JSON.parse(specsData) as UTSJSONObject
} else {
// 假设已经是对象
specs = specsData as UTSJSONObject
}
} catch(e) {
console.error('解析SKU规格失败', e)
}
} catch(e) {
console.error('解析SKU规格失败', e)
}
}
}
return {
id: sku.id,
product_id: sku.product_id,
sku_code: sku.sku_code,
id: sku['id'] as string,
product_id: sku['product_id'] as string,
sku_code: sku['sku_code'] as string,
specifications: specs,
price: sku.price,
stock: sku.stock !== undefined ? sku.stock : 0,
image_url: sku.image_url != null ? sku.image_url : '',
status: sku.status !== undefined ? sku.status : 1
price: sku['price'] as number,
stock: sku['stock'] != null ? sku['stock'] as number : 0,
image_url: sku['image_url'] != null ? sku['image_url'] as string : '',
status: sku['status'] != null ? sku['status'] as number : 1
} as ProductSkuType
})
return
@@ -564,13 +587,18 @@ export default {
async loadCoupons() {
if (this.product.merchant_id == '') return
// Safety check for cached service definition
// @ts-ignore
if (typeof supabaseService.fetchShopCoupons === 'function') {
this.coupons = await supabaseService.fetchShopCoupons(this.product.merchant_id)
} else if (typeof supabaseService.getAvailableCoupons === 'function') {
this.coupons = await supabaseService.getAvailableCoupons(this.product.merchant_id)
} else {
console.warn('SupabaseService.fetchShopCoupons method missing in runtime. Please restart project.')
try {
// @ts-ignore
const couponData = await supabaseService.fetchShopCoupons(this.product.merchant_id)
this.coupons = couponData as Array<CouponTemplateType>
} catch (e) {
try {
// @ts-ignore
const couponData2 = await supabaseService.getAvailableCoupons(this.product.merchant_id)
this.coupons = couponData2 as Array<CouponTemplateType>
} catch (e2) {
console.warn('SupabaseService coupon methods not available:', e2)
}
}
},
@@ -596,7 +624,7 @@ export default {
},
// 新增:领取优惠券
async claimCoupon(coupon: any) {
async claimCoupon(coupon: CouponTemplateType) {
const userId = supabaseService.getCurrentUserId()
if (userId == '') {
uni.navigateTo({ url: '/pages/auth/login' })
@@ -605,17 +633,21 @@ export default {
uni.showLoading({ title: '领取中' })
let success = false
// @ts-ignore
if (typeof supabaseService.claimShopCoupon === 'function') {
success = await supabaseService.claimShopCoupon(coupon.id, userId)
} else if (typeof supabaseService.claimCoupon === 'function') {
success = await supabaseService.claimCoupon(coupon.id, userId)
} else {
console.warn('claimCoupon method missing')
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) {
if (success === true) {
uni.showToast({ title: '领取成功', icon: 'success' })
} else {
uni.showToast({ title: '领取失败或已领取', icon: 'none' })
@@ -629,7 +661,9 @@ export default {
},
onSwiperChange(e: any) {
this.currentImageIndex = e.detail.current
const eventObj = e as UTSJSONObject
const detail = eventObj['detail'] as UTSJSONObject
this.currentImageIndex = detail['current'] as number
},
showSpecModal() {
@@ -647,15 +681,16 @@ export default {
},
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications) {
const specs: any = sku.specifications
return Object.keys(specs).map(key => `${key}: ${specs[key]}`).join(', ')
if (sku.specifications != null) {
const specs = sku.specifications as UTSJSONObject
// 简化处理,直接返回 JSON 字符串
return JSON.stringify(specs)
}
return sku.sku_code
},
async addToCart() {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -673,7 +708,7 @@ export default {
)
uni.hideLoading()
if (success) {
if (success === true) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
console.error('添加购物车返回失败')
@@ -687,7 +722,7 @@ export default {
},
buyNow() {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -695,29 +730,24 @@ export default {
return
}
const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
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 && 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)
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
}
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]
})
}
url: '/pages/mall/consumer/checkout'
})
},
@@ -761,7 +791,7 @@ export default {
},
goToShop() {
if (this.merchant.user_id) {
if (this.merchant.user_id != null && this.merchant.user_id !== '') {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.id}`
})
@@ -788,8 +818,7 @@ export default {
},
validateQuantity() {
let num = parseInt(this.quantity)
if (isNaN(num)) num = 1
let num = this.quantity
const maxQuantity = this.getMaxQuantity()
if (num < 1) num = 1
else if (num > maxQuantity) {
@@ -800,9 +829,9 @@ export default {
},
getMaxQuantity() {
if (this.selectedSkuId) {
if (this.selectedSkuId != null && this.selectedSkuId !== '') {
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
if (sku) return sku.stock
if (sku != null) return sku!.stock
}
return this.product.stock
},