consumer模块完成90%,前端完成supabase对接

This commit is contained in:
2026-02-03 17:11:50 +08:00
parent b6200cda28
commit 8a535e3f38
69 changed files with 5020 additions and 33273 deletions

View File

@@ -52,7 +52,7 @@
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal">
<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>
@@ -276,6 +276,13 @@ export default {
},
methods: {
saveFootprint(productId: string) {
// 调用后端API记录足迹
supabaseService.addFootprint(productId).then(success => {
if (success) {
console.log('足迹已同步到服务器')
}
})
const footprintData = uni.getStorageSync('footprints')
let footprints: any[] = []
@@ -311,330 +318,158 @@ export default {
},
async loadProductDetail(productId: string, options: any = {}) {
// 尝试从数据库加载
let dbProductRaw = null
uni.showLoading({ title: '加载中...' })
try {
console.log('正在尝试从数据库加载商品详情:', productId)
dbProductRaw = await supabaseService.getProductById(productId)
console.log('数据库返回的商品详情 (原始数据):', dbProductRaw)
// 调试:打印数据库返回的所有字段
if (dbProductRaw) {
console.log('数据库返回字段详情:')
if (Array.isArray(dbProductRaw)) {
console.log('返回数据是数组,长度:', dbProductRaw.length)
if (dbProductRaw.length > 0) {
const firstItem = dbProductRaw[0]
console.log('数组第一个元素:', firstItem)
for (const key in firstItem) {
console.log(` ${key}:`, firstItem[key], typeof firstItem[key])
}
const dbProductResponse = await supabaseService.getProductById(productId)
let dbProduct: any = null
if (Array.isArray(dbProductResponse) && dbProductResponse.length > 0) {
dbProduct = dbProductResponse[0]
} else if (dbProductResponse && !Array.isArray(dbProductResponse)) {
dbProduct = dbProductResponse
}
if (dbProduct) {
// 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 || '',
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(),
// 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,
tags: [] as string[]
} as ProductType
// Handle Images
if (dbProduct.image_urls) {
try {
const parsed = typeof dbProduct.image_urls === 'string' ? JSON.parse(dbProduct.image_urls) : dbProduct.image_urls
if (Array.isArray(parsed)) {
this.product.images = parsed.map((i: any) => String(i))
}
} catch (e) { console.error('Error parsing image_urls', e) }
}
} else {
console.log('返回数据是对象')
for (const key in dbProductRaw) {
console.log(` ${key}:`, dbProductRaw[key], typeof dbProductRaw[key])
// 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)
}
}
// Fallback to 'image' field (legacy)
if (this.product.images.length === 0 && dbProduct.image) {
this.product.images.push(dbProduct.image)
}
// Final fallback
if (this.product.images.length === 0) {
this.product.images.push('/static/default-product.png')
}
// Handle Tags
if (dbProduct.tags) {
try {
const parsedTags = typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags
if (Array.isArray(parsedTags)) {
this.product.tags = parsedTags.map((t: any) => String(t))
}
} catch (e) {}
}
// Handle JSON attributes if present
if (dbProduct.attributes && typeof dbProduct.attributes === 'string') {
try {
const attrs = JSON.parse(dbProduct.attributes)
if (attrs) {
// Merge attributes into product if they match keys
if (attrs.specification) this.product.specification = attrs.specification
if (attrs.usage) this.product.usage = attrs.usage
// ... augment as needed
}
} catch(e) {}
}
// Load SKUs
// this.loadProductSkus(productId) // If SKU logic exists
} else {
throw new Error('No product found')
}
} catch (e) {
console.error('Failed to load product from DB', e)
}
// 处理数据库返回数据:可能是数组或对象
let dbProduct = null
if (dbProductRaw) {
if (Array.isArray(dbProductRaw)) {
if (dbProductRaw.length > 0) {
dbProduct = dbProductRaw[0] // 取数组第一个元素
} else {
console.warn('数据库返回空数组')
}
} else {
dbProduct = dbProductRaw // 已经是对象
}
}
if (dbProduct) {
console.log('使用数据库数据渲染页面')
// 调试打印dbProduct的详细结构和类型
console.log('dbProduct类型:', typeof dbProduct)
console.log('dbProduct原型:', Object.getPrototypeOf(dbProduct))
console.log('dbProduct的键:')
for (let key in dbProduct) {
console.log(' ', key, ':', dbProduct[key], '类型:', typeof dbProduct[key])
}
// 验证必要字段,如果关键字段缺失则使用模拟数据
// 注意数据库返回的字段可能与本地ProductType不完全匹配
console.log('验证必要字段dbProduct:', dbProduct)
// 尝试多种方式访问属性
const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
// 价格字段兼容性处理:优先查找 price其次查找 base_price
let priceValue = dbProduct.price
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct.base_price
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['price']
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['base_price']
}
const hasId = idValue !== undefined && idValue !== null
const hasName = nameValue !== undefined && nameValue !== null
const hasPrice = priceValue !== undefined && priceValue !== null
const hasRequiredFields = dbProduct && hasId && hasName && hasPrice
console.log('字段检查 - id:', idValue, 'hasId:', hasId, 'name:', nameValue, 'hasName:', hasName, 'price:', priceValue, 'hasPrice:', hasPrice)
console.log('hasRequiredFields:', hasRequiredFields)
if (!hasRequiredFields) {
console.warn('数据库返回数据缺少必要字段,使用模拟数据')
// 继续执行会进入下面的else分支
dbProduct = null
} else {
// 更新dbProduct的字段为实际值确保后续使用正确的属性访问
if (dbProduct.id === undefined && idValue !== undefined) dbProduct.id = idValue
if (dbProduct.name === undefined && nameValue !== undefined) dbProduct.name = nameValue
if (dbProduct.price === undefined && priceValue !== undefined) dbProduct.price = priceValue
// 使用数据库数据 - 处理字段映射
// 数据库Product接口和本地ProductType接口字段可能不同
const images = [] as Array<string>
// 处理图片字段优先使用image_urls字段其次使用main_image_url
console.log('处理数据库图片字段')
// 尝试从数据库的image_urls字段获取图片JSON字符串或对象
if (dbProduct.image_urls) {
let imagesArray: any[] = []
if (typeof dbProduct.image_urls === 'string') {
try {
imagesArray = JSON.parse(dbProduct.image_urls)
} catch (e) {
console.error('解析image_urls字段失败:', e, dbProduct.image_urls)
// 尝试逗号分割
if (dbProduct.image_urls.includes(',')) {
imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim())
}
}
} else if (Array.isArray(dbProduct.image_urls)) {
imagesArray = dbProduct.image_urls
}
if (imagesArray.length > 0) {
for (const img of imagesArray) {
if (typeof img === 'string' && img) {
images.push(img)
}
}
}
}
// 如果没有获取到相册图,但有主图,放入相册
if (dbProduct.main_image_url) {
// 如果相册里没有这张图,把它加到第一位
if (!images.includes(dbProduct.main_image_url)) {
images.unshift(dbProduct.main_image_url)
}
}
// 兼容旧字段 image
if (images.length === 0 && dbProduct.image) {
images.push(dbProduct.image)
}
// 如果仍然没有图片,使用传入的图片或默认图片
if (images.length === 0) {
if (options.image) {
images.push(decodeURIComponent(options.image as string))
} else {
images.push('/static/product1.jpg')
}
}
// 补充模拟图片如果图片数量不足3张
const needSupplementCount = 3 - images.length
if (needSupplementCount > 0) {
const supplementalImages = ['/static/product2.jpg', '/static/product3.jpg']
for (let i = 0; i < needSupplementCount && i < supplementalImages.length; i++) {
images.push(supplementalImages[i])
}
}
console.log('最终图片数组:', images)
// 映射字段数据库shop_id对应本地merchant_id
const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
// 确保数值字段有效
// 优先使用 price不存在则使用 base_price
let productPrice = 0
if (typeof dbProduct.price === 'number') {
productPrice = dbProduct.price
} else if (typeof dbProduct.base_price === 'number') {
productPrice = dbProduct.base_price
} else if (priceValue !== undefined) {
// 使用上面校验时获取到的 priceValue
productPrice = Number(priceValue)
}
const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100)
const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50)
// 解析 attributes
let attributes: any = {}
if (dbProduct.attributes) {
try {
if (typeof dbProduct.attributes === 'string') {
attributes = JSON.parse(dbProduct.attributes)
} else {
attributes = dbProduct.attributes
}
} catch (e) {
console.error('解析 attributes 失败', e)
}
}
this.product = {
id: dbProduct.id || productId,
merchant_id: merchantId,
category_id: dbProduct.category_id || 'cat_001',
name: dbProduct.name || '商品名称',
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: images,
price: productPrice,
original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null),
stock: stock,
sales: sales,
status: 1,
created_at: dbProduct.created_at || '2024-01-01',
// 药品相关字段
specification: attributes.specification || dbProduct.specification || null,
usage: attributes.usage || dbProduct.usage || null,
side_effects: attributes.side_effects || dbProduct.side_effects || null,
precautions: attributes.precautions || dbProduct.precautions || null,
expiry_date: attributes.expiry_date || dbProduct.expiry_date || null,
storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null,
approval_number: attributes.approval_number || dbProduct.approval_number || null,
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
} as ProductType
console.log('页面 product 对象已更新:', this.product)
console.log('商品图片数组:', this.product.images)
console.log('商品价格:', this.product.price, '库存:', this.product.stock, '销量:', this.product.sales)
}
} else {
console.log('数据库无数据或加载失败,使用模拟数据')
// 数据库无数据时,使用原有模拟逻辑
const generatePriceFromId = (id: string): number => {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0
}
const price = 50 + Math.abs(hash % 450)
return parseFloat(price.toFixed(2))
}
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
const productName = options.name ? decodeURIComponent(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'
}
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']
}
// 尝试加载真实商户信息
// Load Merchant and SKUs
if (this.product.merchant_id) {
await this.loadMerchantInfo(this.product.merchant_id)
}
if (this.product.id) {
this.loadProductSkus(this.product.id)
}
uni.hideLoading()
},
async loadMerchantInfo(merchantId: string) {
let realMerchantLoaded = false
// 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询
if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) {
console.log('尝试加载商户信息:', this.product.merchant_id)
if (merchantId.includes('-') || !merchantId.startsWith('merchant_')) {
try {
const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id)
const shop = await supabaseService.getShopByMerchantId(merchantId)
if (shop) {
console.log('加载到商户信息:', shop.shop_name)
// 确保字段存在,避免 undefined 导致构造失败
this.merchant = {
id: shop.id || '',
user_id: shop.merchant_id || '',
shop_name: shop.shop_name || '未命名店铺',
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 || '',
shop_status: 1,
// 优先使用 avg_rating没有则使用默认值
rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8,
// 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0
total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0),
rating: shop.rating_avg || 5.0,
total_sales: shop.total_sales || 0,
created_at: shop.created_at || new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
}
} catch (e) {
console.error('加载商户信息失败', e)
console.error('Load shop failed', e)
}
}
if (!realMerchantLoaded) {
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const merchantIndex = Math.abs(merchantId.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'),
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: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_description: '优质服务,正品保障',
contact_name: '店主',
contact_phone: '',
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
rating: 4.8,
total_sales: 999,
created_at: '2023-01-01'
} as MerchantType
}
this.loadProductSkus(productId)
},
async loadProductSkus(productId: string) {
@@ -674,32 +509,9 @@ export default {
console.error('Fetch SKUs error', e)
}
// 模拟加载商品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
}
]
// 如果没有从数据库加载到SKU则不显示规格选择直接作为无规格商品添加
// 移除之前的Mock逻辑因为Mock的ID不符合UUID格式会导致数据库错误
},
onSwiperChange(e: any) {
@@ -729,7 +541,7 @@ export default {
},
async addToCart() {
if (!this.selectedSkuId) {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -776,7 +588,7 @@ export default {
},
buyNow() {
if (!this.selectedSkuId) {
if (this.productSkus.length > 0 && !this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
icon: 'none'
@@ -784,7 +596,7 @@ export default {
return
}
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
const sku = this.selectedSkuId ? this.productSkus.find(s => s.id === this.selectedSkuId) : null
// 调试:打印价格信息
console.log('立即购买 - 商品价格信息:')
@@ -829,56 +641,53 @@ export default {
},
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)
}
}
// console.log('product-detail checkFavoriteStatus id:', id)
this.checkFavorite(id)
},
async checkFavorite(id: string) {
const isFav = await supabaseService.checkFavorite(id)
this.isFavorite = isFav
},
toggleFavorite() {
const storedFavorites = uni.getStorageSync('favorites')
let favorites: any[] = []
async toggleFavorite() {
if (!this.product.id) return
if (storedFavorites) {
try {
favorites = JSON.parse(storedFavorites as string) as any[]
} catch (e) {
console.error('Failed to parse favorites', e)
}
// 显示loading
uni.showLoading({ title: '处理中' })
try {
// 记录操作前的状态
const wasFavorite = this.isFavorite
// 执行切换返回的是最新的状态true=已收藏false=未收藏)
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'
})
}
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() {