20260227-1
This commit is contained in:
80
doc_mall/consumer/sql/update_category_icons.sql
Normal file
80
doc_mall/consumer/sql/update_category_icons.sql
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
-- 更新分类图标为 emoji 格式
|
||||||
|
-- 运行此脚本修复分类图标显示问题
|
||||||
|
|
||||||
|
-- 更新一级分类图标
|
||||||
|
UPDATE public.ml_categories
|
||||||
|
SET icon_url =
|
||||||
|
CASE
|
||||||
|
WHEN slug = 'digital' THEN '📱'
|
||||||
|
WHEN slug = 'fashion' THEN '👕'
|
||||||
|
WHEN slug = 'home' THEN '🏠'
|
||||||
|
WHEN slug = 'food' THEN '🍎'
|
||||||
|
WHEN slug = 'beauty' THEN '💄'
|
||||||
|
WHEN slug = 'sports' THEN '⚽'
|
||||||
|
WHEN slug = 'books' THEN '📚'
|
||||||
|
WHEN slug = 'baby' THEN '👶'
|
||||||
|
WHEN slug = 'health' THEN '💊'
|
||||||
|
ELSE icon_url
|
||||||
|
END
|
||||||
|
WHERE level = 1;
|
||||||
|
|
||||||
|
-- 更新二级分类图标
|
||||||
|
UPDATE public.ml_categories
|
||||||
|
SET icon_url =
|
||||||
|
CASE
|
||||||
|
-- 数码电器二级分类
|
||||||
|
WHEN slug = 'mobile' THEN '📱'
|
||||||
|
WHEN slug = 'computer' THEN '💻'
|
||||||
|
WHEN slug = 'appliance' THEN '🎥'
|
||||||
|
WHEN slug = 'accessories' THEN '🔌'
|
||||||
|
-- 服装鞋帽二级分类
|
||||||
|
WHEN slug = 'mens-wear' THEN '👔'
|
||||||
|
WHEN slug = 'womens-wear' THEN '👗'
|
||||||
|
WHEN slug = 'mens-shoes' THEN '👞'
|
||||||
|
WHEN slug = 'womens-shoes' THEN '👠'
|
||||||
|
-- 家居用品二级分类
|
||||||
|
WHEN slug = 'furniture' THEN '🛋️'
|
||||||
|
WHEN slug = 'decoration' THEN '🖼️'
|
||||||
|
WHEN slug = 'kitchen' THEN '🍳'
|
||||||
|
WHEN slug = 'daily' THEN '🧹'
|
||||||
|
-- 食品饮料二级分类
|
||||||
|
WHEN slug = 'fruits' THEN '🍊'
|
||||||
|
WHEN slug = 'meat' THEN '🥩'
|
||||||
|
WHEN slug = 'snacks' THEN '🍪'
|
||||||
|
WHEN slug = 'drinks' THEN '🍺'
|
||||||
|
-- 美妆护肤二级分类
|
||||||
|
WHEN slug = 'skincare' THEN '🧴'
|
||||||
|
WHEN slug = 'makeup' THEN '💅'
|
||||||
|
-- 运动户外二级分类
|
||||||
|
WHEN slug = 'outdoor' THEN '🏕️'
|
||||||
|
WHEN slug = 'fitness' THEN '🏋️'
|
||||||
|
-- 母婴用品二级分类
|
||||||
|
WHEN slug = 'toys' THEN '🧸'
|
||||||
|
WHEN slug = 'feeding' THEN '🍼'
|
||||||
|
-- 图书文娱二级分类
|
||||||
|
WHEN slug = 'stationery' THEN '✏️'
|
||||||
|
WHEN slug = 'audio' THEN '🎵'
|
||||||
|
ELSE icon_url
|
||||||
|
END
|
||||||
|
WHERE level = 2;
|
||||||
|
|
||||||
|
-- 如果有 icon_url 为 icon-xxx 格式的记录,也进行更新
|
||||||
|
UPDATE public.ml_categories
|
||||||
|
SET icon_url =
|
||||||
|
CASE
|
||||||
|
WHEN icon_url = 'icon-digital' THEN '📱'
|
||||||
|
WHEN icon_url = 'icon-fashion' THEN '👕'
|
||||||
|
WHEN icon_url = 'icon-home' THEN '🏠'
|
||||||
|
WHEN icon_url = 'icon-food' THEN '🍎'
|
||||||
|
WHEN icon_url = 'icon-beauty' THEN '💄'
|
||||||
|
WHEN icon_url = 'icon-sports' THEN '⚽'
|
||||||
|
WHEN icon_url = 'icon-books' THEN '📚'
|
||||||
|
WHEN icon_url = 'icon-baby' THEN '👶'
|
||||||
|
WHEN icon_url = 'icon-health' THEN '💊'
|
||||||
|
ELSE icon_url
|
||||||
|
END
|
||||||
|
WHERE icon_url LIKE 'icon-%';
|
||||||
|
|
||||||
|
-- 查看更新结果
|
||||||
|
SELECT name, slug, icon_url FROM public.ml_categories WHERE level = 1 ORDER BY sort_order;
|
||||||
|
SELECT name, slug, icon_url FROM public.ml_categories WHERE level = 2 ORDER BY sort_order;
|
||||||
28
fix_merchant_id.py
Normal file
28
fix_merchant_id.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
# Read the file
|
||||||
|
with open('pages/mall/consumer/product-detail.uvue', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find and replace the selectedItem object
|
||||||
|
# Looking for the pattern with quantity as the last property
|
||||||
|
old_pattern = r"(const selectedItem = \{[^}]+quantity: this\.quantity as number)\s*\}"
|
||||||
|
|
||||||
|
new_text = r"""\1,
|
||||||
|
merchant_id: this.product.merchant_id ?? '',
|
||||||
|
shop_id: this.product.merchant_id ?? '',
|
||||||
|
shop_name: this.merchant?.shop_name ?? ''
|
||||||
|
}"""
|
||||||
|
|
||||||
|
# Check if pattern exists
|
||||||
|
if 'quantity: this.quantity as number' in content and 'selectedItem' in content:
|
||||||
|
content = re.sub(old_pattern, new_text, content, flags=re.DOTALL)
|
||||||
|
print("Pattern found and replaced!")
|
||||||
|
else:
|
||||||
|
print("Pattern not found!")
|
||||||
|
|
||||||
|
# Write back
|
||||||
|
with open('pages/mall/consumer/product-detail.uvue', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("File updated successfully!")
|
||||||
50
fix_merchant_id2.py
Normal file
50
fix_merchant_id2.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
# Read the file
|
||||||
|
with open('pages/mall/consumer/product-detail.uvue', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find the exact text to replace
|
||||||
|
old_text = '''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
|
||||||
|
}'''
|
||||||
|
|
||||||
|
new_text = '''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,
|
||||||
|
merchant_id: this.product.merchant_id ?? '',
|
||||||
|
shop_id: this.product.merchant_id ?? '',
|
||||||
|
shop_name: this.merchant?.shop_name ?? ''
|
||||||
|
}'''
|
||||||
|
|
||||||
|
if old_text in content:
|
||||||
|
content = content.replace(old_text, new_text)
|
||||||
|
print("Found and replaced!")
|
||||||
|
else:
|
||||||
|
print("Old text not found, trying alternative...")
|
||||||
|
# Try with tab characters
|
||||||
|
old_text_alt = old_text.replace(' ', '\t\t\t')
|
||||||
|
if old_text_alt in content:
|
||||||
|
content = content.replace(old_text_alt, new_text.replace(' ', '\t\t\t'))
|
||||||
|
print("Found with tabs and replaced!")
|
||||||
|
else:
|
||||||
|
print("Still not found")
|
||||||
|
|
||||||
|
# Write back
|
||||||
|
with open('pages/mall/consumer/product-detail.uvue', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print("Done!")
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"pages": [
|
|
||||||
{
|
|
||||||
"path": "pages/minimal",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "最小测试"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,7 @@ type RecommendProduct = {
|
|||||||
price: number
|
price: number
|
||||||
image: string
|
image: string
|
||||||
skuId: string
|
skuId: string
|
||||||
|
merchant_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@@ -326,9 +327,10 @@ const loadCartData = async () => {
|
|||||||
shopId: p.merchant_id ?? 'unknown',
|
shopId: p.merchant_id ?? 'unknown',
|
||||||
shopName: p.shop_name ?? '商城推荐',
|
shopName: p.shop_name ?? '商城推荐',
|
||||||
name: p.name,
|
name: p.name,
|
||||||
price: p.base_price ?? p.price ?? 0,
|
price: p.base_price ?? p.market_price ?? 0,
|
||||||
image: p.main_image_url ?? '/static/images/default-product.png',
|
image: p.main_image_url ?? p.image_url ?? '/static/images/default-product.png',
|
||||||
skuId: ''
|
skuId: '',
|
||||||
|
merchant_id: p.merchant_id ?? ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -558,31 +560,52 @@ const deleteSelectedItems = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToCart = async (product: any) => {
|
const addToCart = async (product: RecommendProduct) => {
|
||||||
|
uni.showLoading({ title: '检查商品...' })
|
||||||
try {
|
try {
|
||||||
// 调用SupabaseService添加商品到购物车
|
const productId = product.id
|
||||||
// 显式访问属性,避免any类型导致的编译错误
|
const skuId = product.skuId
|
||||||
const target = product as UTSJSONObject
|
const merchantId = product.merchant_id
|
||||||
const productId = target.getString('id') ?? ''
|
|
||||||
const skuId = target.getString('skuId') ?? ''
|
|
||||||
const success = await supabaseService.addToCart(productId, 1, skuId)
|
|
||||||
if (success) {
|
|
||||||
uni.showToast({
|
|
||||||
title: '已添加到购物车',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重新加载购物车数据
|
// 检查商品是否有SKU
|
||||||
loadCartData()
|
const skus = await supabaseService.getProductSkus(productId)
|
||||||
} else {
|
uni.hideLoading()
|
||||||
console.error('添加商品到购物车失败')
|
|
||||||
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '添加失败',
|
title: '请选择规格',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(productId, 1, skuId, merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新加载购物车数据
|
||||||
|
loadCartData()
|
||||||
|
} else {
|
||||||
|
console.error('添加商品到购物车失败')
|
||||||
|
uni.showToast({
|
||||||
|
title: '添加失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('添加商品到购物车异常:', error)
|
console.error('添加商品到购物车异常:', error)
|
||||||
|
uni.hideLoading()
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '添加失败',
|
title: '添加失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
|
|||||||
@@ -65,30 +65,16 @@
|
|||||||
class="product-card"
|
class="product-card"
|
||||||
@click="navigateToProduct(product)"
|
@click="navigateToProduct(product)"
|
||||||
>
|
>
|
||||||
<view class="product-badge" v-if="product.is_hot">热销</view>
|
|
||||||
<image
|
<image
|
||||||
class="product-image"
|
class="product-image"
|
||||||
:src="product.main_image_url"
|
:src="product.main_image_url"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<view class="product-info">
|
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||||
<text class="product-name">{{ product.name }}</text>
|
<view class="product-bottom">
|
||||||
|
<text class="product-price">¥{{ product.base_price ?? product.price ?? 0 }}</text>
|
||||||
<view class="price-section">
|
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||||
<view class="current-price">
|
<text class="add-icon">+</text>
|
||||||
<text class="price-symbol">¥</text>
|
|
||||||
<text class="price-value">{{ product.base_price ?? product.price ?? 0 }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="original-price" v-if="product.market_price != null && product.base_price != null && product.market_price! > product.base_price!">
|
|
||||||
¥{{ product.market_price }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="product-meta">
|
|
||||||
<text class="manufacturer">{{ product.brand_name ?? product.shop_name ?? '自营' }}</text>
|
|
||||||
<view class="sales-info">
|
|
||||||
<text class="sales-count">已售{{ product.sale_count }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -464,32 +450,53 @@ onShow(() => {
|
|||||||
|
|
||||||
// 添加到购物车
|
// 添加到购物车
|
||||||
async function addToCart(product: Product): Promise<void> {
|
async function addToCart(product: Product): Promise<void> {
|
||||||
uni.showLoading({ title: '添加中...' })
|
uni.showLoading({ title: '检查商品...' })
|
||||||
try {
|
try {
|
||||||
const pid = (product.id ?? '').toString()
|
const pid = (product.id ?? '').toString()
|
||||||
|
const merchantId = product.merchant_id ?? ''
|
||||||
if (pid === '') {
|
if (pid === '') {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showToast({ title: '商品无效', icon: 'none' })
|
uni.showToast({ title: '商品无效', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const success = await supabaseService.addToCart(pid, 1, '')
|
|
||||||
if (success) {
|
// 检查商品是否有SKU
|
||||||
|
const skus = await supabaseService.getProductSkus(pid)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '请选择规格',
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
cartCount.value++
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '添加失败,请先登录',
|
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + pid
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(pid, 1, '', merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
cartCount.value++
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '添加失败,请先登录',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('添加到购物车异常', e)
|
console.error('添加到购物车异常', e)
|
||||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,111 +801,70 @@ function onScan(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-card {
|
.product-card {
|
||||||
background: white;
|
display: flex;
|
||||||
border-radius: 12px;
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* cursor: pointer; removed for uniapp-x support */
|
width: 48%;
|
||||||
transition: all 0.3s ease;
|
margin-bottom: 12px;
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
position: relative;
|
|
||||||
/* margin: 10px; gap replacement - moved to logic */
|
|
||||||
width: 44%; /* Decreased to 44% to ensure it fits (44 + 3 + 3 = 50%) */
|
|
||||||
margin: 3%; /* Increased margin */
|
|
||||||
box-sizing: border-box; /* Ensure border IS included in width */
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
background: #FF5722;
|
|
||||||
color: white;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 160px;
|
height: 170px;
|
||||||
/* object-fit: cover; REMOVED for uniapp-x support - default behavior is often acceptable or handle via image mode */
|
border-radius: 8px;
|
||||||
background: white;
|
margin-bottom: 8px;
|
||||||
}
|
background: #f5f5f5;
|
||||||
|
|
||||||
.product-info {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 5px;
|
||||||
/* display: block; REMOVED for uniapp-x support */
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
height: 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-spec {
|
.product-bottom {
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
/* display: block; REMOVED for uniapp-x support */
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-section {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-end; /* changed from baseline */
|
|
||||||
/* gap: 8px; */
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-price {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end; /* changed from baseline */
|
|
||||||
margin-right: 8px; /* gap replacement */
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-symbol {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #FF5722;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #FF5722;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.original-price {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999;
|
|
||||||
/* text-decoration: line-through; REMOVED for uniapp-x support */
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
padding: 0 8px 8px;
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.manufacturer {
|
.product-price {
|
||||||
color: #666;
|
font-size: 15px;
|
||||||
|
color: #ff5000;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sales-count {
|
.product-add-btn {
|
||||||
color: #999;
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #ff5000;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-action {
|
.product-action {
|
||||||
|
|||||||
@@ -1309,6 +1309,12 @@ const submitOrder = async () => {
|
|||||||
const groups: any[] = []
|
const groups: any[] = []
|
||||||
for (let i = 0; i < shopGroups.value.length; i++) {
|
for (let i = 0; i < shopGroups.value.length; i++) {
|
||||||
const group = shopGroups.value[i]
|
const group = shopGroups.value[i]
|
||||||
|
console.log(`[submitOrder] 处理店铺组 ${i}:`, {
|
||||||
|
shopId: group.shopId,
|
||||||
|
shopName: group.shopName,
|
||||||
|
merchant_id: group.merchant_id,
|
||||||
|
itemsCount: group.items.length
|
||||||
|
})
|
||||||
const items: any[] = []
|
const items: any[] = []
|
||||||
for (let j = 0; j < group.items.length; j++) {
|
for (let j = 0; j < group.items.length; j++) {
|
||||||
const item = group.items[j]
|
const item = group.items[j]
|
||||||
@@ -1323,8 +1329,10 @@ const submitOrder = async () => {
|
|||||||
specifications: item.sku_specifications
|
specifications: item.sku_specifications
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const finalMerchantId = (group.merchant_id != null && group.merchant_id != '') ? group.merchant_id : group.shopId
|
||||||
|
console.log(`[submitOrder] 店铺组 ${i} 最终使用的 merchant_id:`, finalMerchantId)
|
||||||
groups.push({
|
groups.push({
|
||||||
merchant_id: (group.merchant_id != null && group.merchant_id != '') ? group.merchant_id : group.shopId,
|
merchant_id: finalMerchantId,
|
||||||
shopId: group.shopId,
|
shopId: group.shopId,
|
||||||
shopName: group.shopName,
|
shopName: group.shopName,
|
||||||
items: items
|
items: items
|
||||||
|
|||||||
@@ -1394,3 +1394,598 @@ getCurrentUserId 函数 - 将可选链替换为显式 null 检查和 UTSJSONObje
|
|||||||
================================================================================
|
================================================================================
|
||||||
文档结束
|
文档结束
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二十四、2026-02-27 函数可选参数限制(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 可选参数不能跳过传递
|
||||||
|
- UTS Android 不支持跳过可选参数传递
|
||||||
|
- 如果函数有多个可选参数,必须按顺序传递所有参数
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
// 函数定义
|
||||||
|
async addToCart(productId: string, quantity: number = 1, skuId?: string, merchantId?: string): Promise<boolean>
|
||||||
|
|
||||||
|
// 错误调用 - 跳过了 merchantId 参数
|
||||||
|
await supabaseService.addToCart(productId, 1, '')
|
||||||
|
// 编译错误:No value passed for parameter 'merchantId'
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
// 方案1:给可选参数添加默认值
|
||||||
|
async addToCart(productId: string, quantity: number = 1, skuId: string = '', merchantId: string = ''): Promise<boolean>
|
||||||
|
|
||||||
|
// 方案2:调用时传递所有参数
|
||||||
|
await supabaseService.addToCart(productId, 1, '', '')
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 可选参数定义规范
|
||||||
|
- 推荐使用 `param: Type = defaultValue` 而非 `param?: Type`
|
||||||
|
- `param?: Type` 在 Android 端调用时仍需传递参数
|
||||||
|
- `param: Type = defaultValue` 可以在不传参时使用默认值
|
||||||
|
- 示例:
|
||||||
|
```typescript
|
||||||
|
// 不推荐 - 调用时仍需传递参数
|
||||||
|
function foo(a: string, b?: string, c?: string): void
|
||||||
|
|
||||||
|
// 推荐 - 可以跳过参数使用默认值
|
||||||
|
function foo(a: string, b: string = '', c: string = ''): void
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 编译错误提示
|
||||||
|
- 错误信息:"No value passed for parameter 'xxx'"
|
||||||
|
- 原因:可选参数在 Android 端不能跳过
|
||||||
|
- 解决:
|
||||||
|
1. 修改函数签名,使用默认值 `param: Type = defaultValue`
|
||||||
|
2. 调用时传递所有参数
|
||||||
|
|
||||||
|
4. 最佳实践
|
||||||
|
- 对于有多个可选参数的函数,统一使用默认值语法
|
||||||
|
- 调用时显式传递所有参数,避免依赖可选参数跳过
|
||||||
|
- 在服务层函数定义中,优先使用 `= ''` 或 `= 0` 等默认值
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二十五、2026-02-27 模板中的非空断言限制(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 模板中不支持非空断言操作符 `!`
|
||||||
|
- UTS Android 模板中不能使用 `variable!` 非空断言
|
||||||
|
- 错误示例:
|
||||||
|
```html
|
||||||
|
<text v-if="product.original_price != null && product.original_price! > product.price">
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```html
|
||||||
|
<text v-if="product.original_price != null && product.original_price > product.price">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"参数类型不匹配:实际类型为 'Number?',预期类型为 'Number'"
|
||||||
|
- 原因:模板中使用非空断言 `!` 不被支持
|
||||||
|
- 解决:移除非空断言 `!`,直接使用变量进行比较
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 在模板中,先用 `!= null` 判断可空类型,然后直接使用变量
|
||||||
|
- UTS 编译器会在 `!= null` 判断后自动识别变量为非空类型
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二十六、2026-02-27 未导入类型的处理(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 未导入的类型不能直接使用
|
||||||
|
- 在页面中使用的类型必须先导入或使用 UTSJSONObject 替代
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
// Shop 类型未导入
|
||||||
|
const s = shopRespData[i] as Shop
|
||||||
|
const id = s.id // 找不到名称 "id"
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
// 使用 UTSJSONObject
|
||||||
|
const s = shopRespData[i] as UTSJSONObject
|
||||||
|
const id = s.getString('id') ?? ''
|
||||||
|
const name = s.getString('shop_name') ?? ''
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"找不到名称 'XXX'"
|
||||||
|
- 原因:类型未导入或类型定义不存在
|
||||||
|
- 解决:
|
||||||
|
1. 导入需要的类型:`import { Shop } from '@/utils/supabaseService.uts'`
|
||||||
|
2. 使用 UTSJSONObject 替代:`as UTSJSONObject` 然后用 `getString()`、`getNumber()` 访问属性
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 对于简单的数据转换,推荐使用 UTSJSONObject
|
||||||
|
- 避免在多个文件中重复定义相同的类型
|
||||||
|
- 如果需要类型安全,从服务层导入类型定义
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二十七、2026-02-27 服务层数据字段完整性(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 服务层返回数据必须包含所有必要字段
|
||||||
|
- 从数据库获取数据时,必须正确映射所有需要的字段
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const product: Product = {
|
||||||
|
id: prodObj.getString('id') ?? '',
|
||||||
|
name: prodObj.getString('name') ?? '',
|
||||||
|
// 错误:merchant_id 硬编码为空字符串
|
||||||
|
merchant_id: ''
|
||||||
|
} as Product
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const product: Product = {
|
||||||
|
id: prodObj.getString('id') ?? '',
|
||||||
|
name: prodObj.getString('name') ?? '',
|
||||||
|
// 正确:从数据库获取 merchant_id
|
||||||
|
merchant_id: prodObj.getString('merchant_id') ?? ''
|
||||||
|
} as Product
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 调用服务层方法时必须传递完整参数
|
||||||
|
- 页面调用服务层方法时,需要传递所有必要参数
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
// 错误:merchant_id 传空字符串
|
||||||
|
await supabaseService.addToCart(productId, 1, '', '')
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
// 正确:从商品对象获取 merchant_id
|
||||||
|
const merchantId = product.merchant_id ?? ''
|
||||||
|
await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 编译错误提示
|
||||||
|
- 问题表现:数据添加到数据库失败,或添加的数据不完整
|
||||||
|
- 原因:服务层或页面层缺少必要字段的传递
|
||||||
|
- 解决:
|
||||||
|
1. 检查服务层数据映射是否完整
|
||||||
|
2. 检查页面调用时是否传递了所有必要参数
|
||||||
|
|
||||||
|
4. 最佳实践
|
||||||
|
- 服务层方法返回的对象应包含数据库视图的所有字段
|
||||||
|
- 页面调用服务层方法时,应从数据对象中获取并传递所有参数
|
||||||
|
- 对于关联数据(如 merchant_id),确保在数据加载时一并获取
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二十八、2026-02-27 模板中的非运算符限制(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 模板中不支持 `!` 非运算符
|
||||||
|
- UTS Android 模板中不能使用 `!variable` 非运算符
|
||||||
|
- 错误示例:
|
||||||
|
```html
|
||||||
|
<view v-if="!brand.logo_url">
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```html
|
||||||
|
<view v-if="brand.logo_url == null || brand.logo_url == ''">
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"找不到名称'not'"
|
||||||
|
- 原因:模板中不支持非运算符 `!`
|
||||||
|
- 解决:使用显式的比较表达式替代
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 使用 `== null` 或 `== ''` 检查空值
|
||||||
|
- 使用 `!= null && != ''` 检查非空值
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
二十九、2026-02-27 索引访问限制(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 不支持 `(obj as any)['key']` 索引访问方式
|
||||||
|
- UTS Android 不支持对 any 类型使用索引访问
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const detail = (e as any)['detail']
|
||||||
|
val = detail['value'] ?? ''
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
// 方案1:使用 UTSJSONObject
|
||||||
|
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
|
||||||
|
const detail = eObj.get('detail') as UTSJSONObject
|
||||||
|
val = detail.getString('value') ?? ''
|
||||||
|
|
||||||
|
// 方案2:先判断类型再转换
|
||||||
|
if (e instanceof UTSJSONObject) {
|
||||||
|
const eObj = e as UTSJSONObject
|
||||||
|
const detail = eObj.get('detail') as UTSJSONObject
|
||||||
|
val = detail.getString('value') ?? ''
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch"
|
||||||
|
- 原因:any 类型不支持索引访问
|
||||||
|
- 解决:转换为 UTSJSONObject 后使用 `.get()` 方法
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 统一使用 UTSJSONObject 处理动态对象
|
||||||
|
- 使用 `.get()`、`.getString()`、`.getNumber()` 方法访问属性
|
||||||
|
- 对于复杂对象,先用 `JSON.parse(JSON.stringify(obj))` 转换
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十、2026-02-27 字符串不能直接作为布尔条件(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 字符串不能直接作为 if 条件
|
||||||
|
- UTS Android 不支持将字符串直接作为布尔条件判断
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const paramId = '123'
|
||||||
|
if (paramId) { // 错误:字符串不能直接作为布尔条件
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const paramId = '123'
|
||||||
|
if (paramId != null && paramId != '') { // 正确:显式判断
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"Condition type mismatch: inferred type is 'String' but 'Boolean' was expected"
|
||||||
|
- 原因:字符串类型不能直接作为布尔条件
|
||||||
|
- 解决:使用显式的比较表达式
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 使用 `!= null && != ''` 检查字符串非空
|
||||||
|
- 使用 `== null || == ''` 检查字符串为空
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十一、2026-02-27 函数定义顺序(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 函数必须在调用前定义
|
||||||
|
- UTS Android 要求函数在调用之前完成定义
|
||||||
|
- 这与 JavaScript 的函数提升不同
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
onMounted(() => {
|
||||||
|
loadData() // 错误:loadData 还未定义
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const loadData = async () => {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData() // 正确:loadData 已定义
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"找不到名称'xxx'"
|
||||||
|
- 原因:函数在调用点之后定义
|
||||||
|
- 解决:将函数定义移到调用之前
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 将所有函数定义放在生命周期钩子(onMounted、onShow 等)之前
|
||||||
|
- 按依赖关系排序函数定义顺序
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十二、2026-02-27 联合类型属性访问(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 联合类型不能直接访问属性
|
||||||
|
- 当参数类型为联合类型(如 `A | B`)时,不能直接访问属性
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
type A = { id: string, name: string }
|
||||||
|
type B = { id: string, title: string }
|
||||||
|
|
||||||
|
const foo = (item: A | B) => {
|
||||||
|
const id = item.id // 错误:联合类型不能直接访问属性
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const foo = (item: A | B) => {
|
||||||
|
// 方案1:转换为 UTSJSONObject
|
||||||
|
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
|
const id = obj.getString('id') ?? ''
|
||||||
|
|
||||||
|
// 方案2:使用类型守卫
|
||||||
|
if ('name' in item) {
|
||||||
|
const id = item.id // 此时类型已收窄为 A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"找不到名称'xxx'"
|
||||||
|
- 原因:联合类型的属性访问受限
|
||||||
|
- 解决:转换为 UTSJSONObject 或使用类型守卫
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 对于联合类型参数,统一转换为 UTSJSONObject 处理
|
||||||
|
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十三、2026-02-27 any 类型变量不能赋值为 null(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. any 类型变量不能赋值为 null
|
||||||
|
- UTS Android 中 `any` 类型不能赋值为 `null`
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
let res: any = null // 错误:Null cannot be a value of a non-null type 'Any'
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
let res: any = {} // 正确:使用空对象
|
||||||
|
// 或者
|
||||||
|
let res: any | null = null // 使用联合类型
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"Null cannot be a value of a non-null type 'Any'"
|
||||||
|
- 原因:any 类型不允许 null 值
|
||||||
|
- 解决:使用空对象 `{}` 或联合类型 `any | null`
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十四、2026-02-27 对象字面量类型推断问题(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 对象字面量直接赋值给 ref 可能类型不匹配
|
||||||
|
- 当对象字面量直接赋值给特定类型的 ref 时,可能报类型不匹配错误
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
merchant.value = {
|
||||||
|
id: shop.id,
|
||||||
|
user_id: shop.merchant_id,
|
||||||
|
// ...
|
||||||
|
} // 错误:Assignment type mismatch
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
// 方案1:显式声明类型
|
||||||
|
const merchantData: MerchantType = {
|
||||||
|
id: shop.id,
|
||||||
|
user_id: shop.merchant_id,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
merchant.value = merchantData
|
||||||
|
|
||||||
|
// 方案2:使用 as 类型断言
|
||||||
|
merchant.value = {
|
||||||
|
id: shop.id,
|
||||||
|
user_id: shop.merchant_id,
|
||||||
|
// ...
|
||||||
|
} as MerchantType
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"Assignment type mismatch: actual type is '<anonymous>', but 'XXX' was expected"
|
||||||
|
- 原因:对象字面量被推断为匿名类型
|
||||||
|
- 解决:显式声明类型或使用类型断言
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十五、2026-02-27 any 类型不能直接访问属性(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. any 类型参数不能直接访问属性
|
||||||
|
- 在 map、forEach 等回调中,any 类型的参数不能直接访问属性
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const list = rawList.map((item): ProductType => {
|
||||||
|
const id = item.id // 错误:找不到名称"id"
|
||||||
|
const name = item.name // 错误:找不到名称"name"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const list = rawList.map((item: any): ProductType => {
|
||||||
|
// 方案1:转换为 UTSJSONObject
|
||||||
|
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
|
const id = itemObj.getString('id') ?? ''
|
||||||
|
const name = itemObj.getString('name') ?? ''
|
||||||
|
|
||||||
|
// 方案2:显式标注参数类型并使用索引
|
||||||
|
// 注意:这种方式在 UTS Android 中也可能有问题
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"找不到名称'xxx'"
|
||||||
|
- 原因:any 类型的属性访问受限
|
||||||
|
- 解决:转换为 UTSJSONObject 后使用 `.getString()` 等方法
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十六、2026-02-27 类型断言不会添加方法(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. `as UTSJSONObject` 不会给对象添加方法
|
||||||
|
- 使用 `as UTSJSONObject` 只是类型断言,不会让普通对象获得 `getString` 等方法
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const profileObj = profile as UTSJSONObject
|
||||||
|
const id = profileObj.getString('user_id') // 运行时错误:getString is not a function
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
// 必须使用 JSON.parse(JSON.stringify()) 进行真正的转换
|
||||||
|
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
|
||||||
|
const id = profileObj.getString('user_id') ?? ''
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 运行时错误提示
|
||||||
|
- 错误信息:"XXX is not a function"
|
||||||
|
- 原因:类型断言只是编译时行为,不会改变运行时对象的方法
|
||||||
|
- 解决:使用 `JSON.parse(JSON.stringify())` 进行真正的对象转换
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 对于从 API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||||||
|
- 使用 `instanceof UTSJSONObject` 检查对象类型
|
||||||
|
- 不要依赖 `as` 类型断言来添加方法
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十七、2026-02-27 类型必须包含所有必填字段(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 创建类型实例时必须包含所有必填字段
|
||||||
|
- UTS 类型定义中的非可选字段(不带 `?`)都是必填的
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
export type ProductType = {
|
||||||
|
id: string
|
||||||
|
merchant_id: string // 必填
|
||||||
|
category_id: string // 必填
|
||||||
|
name: string
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误:缺少 merchant_id、category_id 等必填字段
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
price: item.price
|
||||||
|
} as ProductType // 运行时错误:missing required property
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
return {
|
||||||
|
id: itemObj.getString('id') ?? '',
|
||||||
|
merchant_id: itemObj.getString('merchant_id') ?? '',
|
||||||
|
category_id: itemObj.getString('category_id') ?? '',
|
||||||
|
name: itemObj.getString('name') ?? '未知商品',
|
||||||
|
description: itemObj.getString('description') ?? '',
|
||||||
|
images: images,
|
||||||
|
price: itemObj.getNumber('base_price') ?? 0,
|
||||||
|
original_price: itemObj.getNumber('market_price') ?? 0,
|
||||||
|
stock: itemObj.getNumber('total_stock') ?? 0,
|
||||||
|
sales: itemObj.getNumber('sale_count') ?? 0,
|
||||||
|
status: 1,
|
||||||
|
created_at: itemObj.getString('created_at') ?? ''
|
||||||
|
} as ProductType
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 运行时错误提示
|
||||||
|
- 错误信息:"Failed to construct type, missing required property: xxx"
|
||||||
|
- 原因:类型定义中有必填字段未提供
|
||||||
|
- 解决:
|
||||||
|
1. 检查类型定义,确认所有必填字段
|
||||||
|
2. 为所有必填字段提供值,即使是空字符串或默认值
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 查看类型定义,确认哪些字段是必填的(不带 `?`)
|
||||||
|
- 使用 `??` 运算符提供默认值
|
||||||
|
- 对于可选字段,可以不提供或使用 `null`
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十八、2026-02-27 回调函数不能是 async(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. API 回调函数不能使用 async 修饰
|
||||||
|
- uni API 的回调函数(如 showModal 的 success)不支持 async 函数
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认',
|
||||||
|
content: '确定要删除吗?',
|
||||||
|
success: async (res) => { // 错误:回调函数不能是 async
|
||||||
|
if (res.confirm) {
|
||||||
|
const result = await someAsyncFunction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认',
|
||||||
|
content: '确定要删除吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 使用 Promise.then() 代替 await
|
||||||
|
someAsyncFunction().then((result) => {
|
||||||
|
// 处理结果
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 编译错误提示
|
||||||
|
- 错误信息:"参数类型不匹配:实际类型为 'Function1<..., UTSPromise<Unit>>',预期类型为 'Function1<..., Unit>?'"
|
||||||
|
- 原因:回调函数返回 Promise 而非 void
|
||||||
|
- 解决:使用 `.then()` 代替 `await`
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 在回调函数中使用 `.then()` 处理异步操作
|
||||||
|
- 将异步逻辑封装为单独的函数,在回调中调用
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
三十九、2026-02-27 类型转换前必须检查类型(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. 使用 `as` 类型转换前必须检查实际类型
|
||||||
|
- 直接使用 `as string` 转换可能导致运行时类型转换异常
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const idVal = item['id']
|
||||||
|
const id = idVal as string // 错误:如果 idVal 是其他类型会崩溃
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const idVal = item['id']
|
||||||
|
const id = (idVal != null && typeof idVal == 'string') ? (idVal as string) : ''
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 运行时错误提示
|
||||||
|
- 错误信息:"null cannot be cast to non-null type kotlin.String"
|
||||||
|
- 错误信息:"java.lang.Boolean cannot be cast to java.lang.String"
|
||||||
|
- 原因:直接类型转换时,实际类型与目标类型不匹配
|
||||||
|
- 解决:使用 `typeof` 检查类型后再转换
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 使用 `typeof` 检查类型
|
||||||
|
- 使用 `!= null` 检查空值
|
||||||
|
- 提供默认值防止空指针异常
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
四十、2026-02-27 UTSJSONObject 必须正确转换(重要)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. `as UTSJSONObject` 不会添加方法
|
||||||
|
- 从数据库返回的数据需要正确转换为 UTSJSONObject
|
||||||
|
- 错误示例:
|
||||||
|
```typescript
|
||||||
|
const item = rawList[i]
|
||||||
|
const brandObj = item as UTSJSONObject // 错误:brandObj.getString 不存在
|
||||||
|
```
|
||||||
|
- 正确示例:
|
||||||
|
```typescript
|
||||||
|
const item = rawList[i]
|
||||||
|
const brandObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
|
const id = brandObj.getString('id') ?? ''
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 运行时错误提示
|
||||||
|
- 错误信息:"getString is not a function"
|
||||||
|
- 原因:对象没有正确转换为 UTSJSONObject
|
||||||
|
- 解决:使用 `JSON.parse(JSON.stringify())` 进行转换
|
||||||
|
|
||||||
|
3. 最佳实践
|
||||||
|
- 对于从数据库/API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||||||
|
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|||||||
@@ -8,20 +8,12 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else v-for="(product, index) in favorites" :key="index" class="product-item" @click="goToDetail(product.id)">
|
<view v-else v-for="(product, index) in favorites" :key="index" class="product-item" @click="goToDetail(product.id)">
|
||||||
<image :src="product.image" class="product-image" mode="aspectFill" />
|
<image :src="product.main_image_url" class="product-image" mode="aspectFill" />
|
||||||
<view class="product-info">
|
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||||
<text class="product-name">{{ product.name }}</text>
|
<view class="product-bottom">
|
||||||
<text class="product-price">¥{{ product.price }}</text>
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
<view class="product-footer">
|
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||||
<text class="product-sales">已售 {{ product.sales }}</text>
|
<text class="add-icon">+</text>
|
||||||
<view class="action-btns">
|
|
||||||
<view class="cart-btn" @click.stop="addToCart(product)">
|
|
||||||
<text class="cart-icon">🛒</text>
|
|
||||||
</view>
|
|
||||||
<view class="remove-btn" @click.stop="removeFavorite(product.id)">
|
|
||||||
<text class="remove-icon">🗑️</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -32,27 +24,42 @@
|
|||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||||
|
import type { Product } from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
type Product = {
|
const favorites = ref<Array<Product>>([])
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
price: number
|
|
||||||
image: string
|
|
||||||
sales: number
|
|
||||||
shopId?: string
|
|
||||||
shopName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const favorites = ref<Product[]>([])
|
|
||||||
|
|
||||||
const addToCart = async (product: Product) => {
|
const addToCart = async (product: Product) => {
|
||||||
uni.showLoading({ title: '添加中' })
|
uni.showLoading({ title: '检查商品...' })
|
||||||
const success = await supabaseService.addToCart(product.id, 1, '')
|
try {
|
||||||
uni.hideLoading()
|
const merchantId = product.merchant_id ?? product.shop_id ?? ''
|
||||||
if (success) {
|
|
||||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
// 检查商品是否有SKU
|
||||||
} else {
|
const skus = await supabaseService.getProductSkus(product.id)
|
||||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
|
uni.showToast({ title: '请选择规格', icon: 'none' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + product.id
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('添加到购物车异常', e)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +83,7 @@ const loadFavorites = async () => {
|
|||||||
let name = '未知商品'
|
let name = '未知商品'
|
||||||
let price = 0
|
let price = 0
|
||||||
let sales = 0
|
let sales = 0
|
||||||
|
let merchantId = ''
|
||||||
|
|
||||||
if (prod != null) {
|
if (prod != null) {
|
||||||
let prodObj: UTSJSONObject
|
let prodObj: UTSJSONObject
|
||||||
@@ -90,6 +98,7 @@ const loadFavorites = async () => {
|
|||||||
price = prodObj.getNumber('base_price') ?? 0
|
price = prodObj.getNumber('base_price') ?? 0
|
||||||
image = prodObj.getString('main_image_url') ?? image
|
image = prodObj.getString('main_image_url') ?? image
|
||||||
sales = prodObj.getNumber('sale_count') ?? 0
|
sales = prodObj.getNumber('sale_count') ?? 0
|
||||||
|
merchantId = prodObj.getString('merchant_id') ?? ''
|
||||||
|
|
||||||
if (image === '/static/default-product.png') {
|
if (image === '/static/default-product.png') {
|
||||||
const imgUrls = prodObj.getString('image_urls')
|
const imgUrls = prodObj.getString('image_urls')
|
||||||
@@ -112,10 +121,10 @@ const loadFavorites = async () => {
|
|||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
price: price,
|
price: price,
|
||||||
image: image,
|
category_id: '',
|
||||||
sales: sales,
|
merchant_id: merchantId,
|
||||||
shopId: '',
|
main_image_url: image,
|
||||||
shopName: ''
|
sale_count: sales
|
||||||
} as Product
|
} as Product
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -213,107 +222,79 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-item {
|
.product-item {
|
||||||
width: 48%; /* Default Mobile: 2 items per row */
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box; /* Important for grid */
|
background: #fff;
|
||||||
}
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
/* PC/Tablet Responsive */
|
width: 48%;
|
||||||
@media (min-width: 768px) {
|
margin-bottom: 12px;
|
||||||
.product-item {
|
|
||||||
width: 31% !important; /* Tablet: 3 items (gap 15px roughly distributed) */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.product-item {
|
|
||||||
width: 15% !important; /* PC: 6 items */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Center content on large screens */
|
|
||||||
.product-grid, .header {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 170px;
|
height: 170px;
|
||||||
background-color: #f5f5f5;
|
border-radius: 8px;
|
||||||
}
|
margin-bottom: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
.product-info {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 5px;
|
||||||
text-overflow: ellipsis;
|
line-height: 1.4;
|
||||||
lines: 2;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 40px;
|
text-overflow: ellipsis;
|
||||||
line-height: 20px;
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-price {
|
.product-price {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
color: #ff5000;
|
color: #ff5000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-footer {
|
.product-add-btn {
|
||||||
display: flex;
|
width: 24px;
|
||||||
justify-content: space-between;
|
height: 24px;
|
||||||
align-items: center;
|
background-color: #ff5000;
|
||||||
margin-top: auto;
|
border-radius: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.product-sales {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-btn, .remove-btn {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 14px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 8px; /* Replacement for gap */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-btn {
|
.add-icon {
|
||||||
background-color: #ff5000;
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-icon {
|
/* PC/Tablet Responsive */
|
||||||
font-size: 14px;
|
@media (min-width: 768px) {
|
||||||
color: white;
|
.product-item {
|
||||||
|
width: 31% !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-btn {
|
@media (min-width: 1024px) {
|
||||||
background-color: #f0f0f0;
|
.product-item {
|
||||||
}
|
width: 15% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.remove-icon {
|
.product-grid, .header {
|
||||||
font-size: 14px;
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,12 +33,11 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="item-content" @click="viewProduct(item)">
|
<view class="item-content" @click="viewProduct(item)">
|
||||||
<image class="product-image" :src="item.image" mode="aspectFill" />
|
<image class="product-image" :src="item.image" mode="aspectFill" />
|
||||||
<view class="product-info">
|
<text class="product-name" :lines="2">{{ item.name }}</text>
|
||||||
<text class="product-name">{{ item.name }}</text>
|
<view class="product-bottom">
|
||||||
<view class="product-bottom">
|
<text class="product-price">¥{{ item.price }}</text>
|
||||||
<view class="product-price-row">
|
<view class="product-add-btn" @click.stop="addToCart(item)">
|
||||||
<text class="current-price">¥{{ item.price }}</text>
|
<text class="add-icon">+</text>
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -83,6 +82,7 @@ type FootprintType = {
|
|||||||
shopName: string
|
shopName: string
|
||||||
viewTime: number
|
viewTime: number
|
||||||
selected: boolean
|
selected: boolean
|
||||||
|
merchant_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FootprintGroup = {
|
type FootprintGroup = {
|
||||||
@@ -178,12 +178,25 @@ const clearAll = () => {
|
|||||||
content: '确定要清空所有浏览记录吗?',
|
content: '确定要清空所有浏览记录吗?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
footprints.value = []
|
uni.showLoading({ title: '清空中...' })
|
||||||
uni.removeStorageSync('footprints')
|
|
||||||
|
|
||||||
uni.showToast({
|
supabaseService.clearFootprints().then((success) => {
|
||||||
title: '已清空',
|
uni.hideLoading()
|
||||||
icon: 'success'
|
|
||||||
|
if (success) {
|
||||||
|
footprints.value = []
|
||||||
|
uni.removeStorageSync('footprints')
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '已清空',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,42 +251,108 @@ const deleteSelected = () => {
|
|||||||
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
|
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showLoading({ title: '删除中' })
|
uni.showLoading({ title: '删除中...' })
|
||||||
|
|
||||||
uni.hideLoading()
|
// 收集要删除的商品ID
|
||||||
|
const productIds: string[] = []
|
||||||
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
|
for (let i = 0; i < selectedItems.length; i++) {
|
||||||
|
productIds.push(selectedItems[i].id)
|
||||||
const dataToSave: FootprintSaveType[] = []
|
|
||||||
for (let i = 0; i < footprints.value.length; i++) {
|
|
||||||
const item = footprints.value[i]
|
|
||||||
dataToSave.push({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
price: item.price,
|
|
||||||
original_price: item.original_price,
|
|
||||||
image: item.image,
|
|
||||||
sales: item.sales,
|
|
||||||
shopId: item.shopId,
|
|
||||||
shopName: item.shopName,
|
|
||||||
viewTime: item.viewTime
|
|
||||||
} as FootprintSaveType)
|
|
||||||
}
|
}
|
||||||
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
|
|
||||||
|
|
||||||
uni.showToast({
|
// 调用服务层批量删除
|
||||||
title: '删除成功',
|
supabaseService.deleteFootprints(productIds).then((success) => {
|
||||||
icon: 'success'
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 从本地列表中移除
|
||||||
|
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
|
||||||
|
|
||||||
|
// 更新本地缓存
|
||||||
|
const dataToSave: FootprintSaveType[] = []
|
||||||
|
for (let i = 0; i < footprints.value.length; i++) {
|
||||||
|
const item = footprints.value[i]
|
||||||
|
dataToSave.push({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
price: item.price,
|
||||||
|
original_price: item.original_price,
|
||||||
|
image: item.image,
|
||||||
|
sales: item.sales,
|
||||||
|
shopId: item.shopId,
|
||||||
|
shopName: item.shopName,
|
||||||
|
viewTime: item.viewTime
|
||||||
|
} as FootprintSaveType)
|
||||||
|
}
|
||||||
|
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '删除成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (footprints.value.length === 0) {
|
||||||
|
isEditMode.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '删除失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (footprints.value.length === 0) {
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addToCart = async (item: FootprintType) => {
|
||||||
|
uni.showLoading({ title: '检查商品...' })
|
||||||
|
try {
|
||||||
|
const productId = item.id
|
||||||
|
const merchantId = item.merchant_id ?? item.shopId ?? ''
|
||||||
|
|
||||||
|
// 检查商品是否有SKU
|
||||||
|
const skus = await supabaseService.getProductSkus(productId)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
|
uni.showToast({
|
||||||
|
title: '请选择规格',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '添加失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('添加到购物车异常', e)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: '操作失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const viewProduct = (item: FootprintType) => {
|
const viewProduct = (item: FootprintType) => {
|
||||||
if (isEditMode.value) return
|
if (isEditMode.value) return
|
||||||
|
|
||||||
@@ -309,7 +388,8 @@ const parseFootprintItem = (item: any): FootprintType => {
|
|||||||
shopId: itemObj.getString('shopId') ?? '',
|
shopId: itemObj.getString('shopId') ?? '',
|
||||||
shopName: itemObj.getString('shopName') ?? '',
|
shopName: itemObj.getString('shopName') ?? '',
|
||||||
viewTime: itemObj.getNumber('viewTime') ?? 0,
|
viewTime: itemObj.getNumber('viewTime') ?? 0,
|
||||||
selected: false
|
selected: false,
|
||||||
|
merchant_id: itemObj.getString('merchant_id') ?? ''
|
||||||
} as FootprintType
|
} as FootprintType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,10 +573,11 @@ onMounted(() => {
|
|||||||
.footprint-item {
|
.footprint-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 10px;
|
background: #fff;
|
||||||
border-bottom: none;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
width: 48%;
|
width: 48%;
|
||||||
background-color: #fff;
|
margin-bottom: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +616,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-content {
|
.item-content {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -543,48 +623,50 @@ onMounted(() => {
|
|||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 170px;
|
height: 170px;
|
||||||
border-radius: 5px;
|
border-radius: 8px;
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
background-color: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
|
||||||
|
|
||||||
.product-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #333333;
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin-bottom: 6px;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
lines: 2;
|
padding: 0 8px;
|
||||||
height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-bottom {
|
.product-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-price-row {
|
.product-price {
|
||||||
display: flex;
|
font-size: 15px;
|
||||||
align-items: flex-end;
|
color: #ff5000;
|
||||||
}
|
font-weight: bold;
|
||||||
|
}
|
||||||
.current-price {
|
|
||||||
font-size: 16px;
|
.product-add-btn {
|
||||||
color: #ff4757;
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #ff5000;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|||||||
@@ -128,10 +128,10 @@
|
|||||||
@click="switchBrand(brand)"
|
@click="switchBrand(brand)"
|
||||||
style="--card-color: #5785e5"
|
style="--card-color: #5785e5"
|
||||||
>
|
>
|
||||||
<image v-if="brand.logo_url" :src="brand.logo_url" mode="aspectFit" class="brand-logo" style="width: 40px; height: 40px; border-radius: 20px;" />
|
<view class="card-icon" v-if="brand.logo_url == null || brand.logo_url == ''">
|
||||||
<view v-else class="card-icon">
|
<text class="card-icon-text">{{ getBrandIcon(brand.name) }}</text>
|
||||||
<text class="card-icon-text">🏢</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<image v-else :src="brand.logo_url" mode="aspectFit" class="brand-logo" style="width: 40px; height: 40px; border-radius: 20px;" />
|
||||||
<text class="card-name">{{ brand.name }}</text>
|
<text class="card-name">{{ brand.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -200,38 +200,16 @@
|
|||||||
class="product-card"
|
class="product-card"
|
||||||
@click="navigateToProduct(product)"
|
@click="navigateToProduct(product)"
|
||||||
>
|
>
|
||||||
<view class="product-badge" v-if="product.is_hot">热销</view>
|
|
||||||
<image
|
<image
|
||||||
class="product-image"
|
class="product-image"
|
||||||
:src="product.main_image_url"
|
:src="product.main_image_url"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
<view class="product-info">
|
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||||
<view class="product-name">{{ product.name }}</view>
|
<view class="product-bottom">
|
||||||
<!-- spec is omitted if not available -->
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
|
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||||
<view class="price-section">
|
<text class="add-icon">+</text>
|
||||||
<view class="current-price">
|
|
||||||
<text class="price-symbol">¥</text>
|
|
||||||
<text class="price-value">{{ product.base_price ?? product.price ?? 0 }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="original-price" v-if="product.market_price != null && product.base_price != null && product.market_price! > product.base_price!">
|
|
||||||
¥{{ product.market_price }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="product-meta">
|
|
||||||
<text class="manufacturer">{{ product.brand_name ?? product.shop_name ?? '自营' }}</text>
|
|
||||||
<view class="sales-info">
|
|
||||||
<text class="sales-count">已售{{ product.sale_count }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="product-action">
|
|
||||||
<view class="cart-btn" @click.stop="addToCart(product)">
|
|
||||||
<text class="cart-icon">+</text>
|
|
||||||
<text class="cart-text">加入购物车</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -333,7 +311,7 @@ const subCategories = ref<Category[]>([])
|
|||||||
const selectedParentCategory = ref<Category | null>(null)
|
const selectedParentCategory = ref<Category | null>(null)
|
||||||
const showSubCategories = ref(false)
|
const showSubCategories = ref(false)
|
||||||
|
|
||||||
// 排序标签类型
|
|
||||||
type SortTab = {
|
type SortTab = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -371,7 +349,7 @@ const healthNews = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 获取分类数据
|
// 获取一级分类数据
|
||||||
const loadCategories = async (): Promise<void> => {
|
const loadCategories = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const categoriesData = await supabaseService.getParentCategories()
|
const categoriesData = await supabaseService.getParentCategories()
|
||||||
@@ -397,6 +375,35 @@ const loadSubCategories = async (parentId: string): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击一级分类
|
||||||
|
const onParentCategoryClick = async (category: Category): Promise<void> => {
|
||||||
|
// 如果已经选中,则切换显示/隐藏二级分类
|
||||||
|
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
|
||||||
|
showSubCategories.value = !showSubCategories.value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中新的分类
|
||||||
|
selectedParentCategory.value = category
|
||||||
|
showSubCategories.value = true
|
||||||
|
|
||||||
|
// 加载二级分类
|
||||||
|
await loadSubCategories(category.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击二级分类
|
||||||
|
const onSubCategoryClick = (category: Category): void => {
|
||||||
|
// 跳转到分类页面
|
||||||
|
uni.setStorageSync('selectedCategory', category.id)
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const randomParam = Math.random().toString(36).substring(2, 8)
|
||||||
|
const url = `/pages/mall/consumer/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}×tamp=${timestamp}&random=${randomParam}`
|
||||||
|
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/mall/consumer/category'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 获取品牌数据
|
// 获取品牌数据
|
||||||
const loadBrands = async (): Promise<void> => {
|
const loadBrands = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -408,6 +415,31 @@ const loadBrands = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据品牌名称获取图标
|
||||||
|
const getBrandIcon = (name: string): string => {
|
||||||
|
if (name == null || name === '') {
|
||||||
|
return '🏢'
|
||||||
|
}
|
||||||
|
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
|
||||||
|
const iconKeys = ['感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签']
|
||||||
|
const iconValues = ['💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺']
|
||||||
|
|
||||||
|
// 尝试精确匹配
|
||||||
|
for (let i = 0; i < iconKeys.length; i++) {
|
||||||
|
if (name === iconKeys[i]) {
|
||||||
|
return iconValues[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 尝试模糊匹配
|
||||||
|
for (let i = 0; i < iconKeys.length; i++) {
|
||||||
|
if (name.indexOf(iconKeys[i]) !== -1) {
|
||||||
|
return iconValues[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 默认返回品牌图标
|
||||||
|
return '🏢'
|
||||||
|
}
|
||||||
|
|
||||||
// 默认加载商品数量
|
// 默认加载商品数量
|
||||||
const defaultLoadLimit: number = 6
|
const defaultLoadLimit: number = 6
|
||||||
|
|
||||||
@@ -648,35 +680,6 @@ const resetNavbar = () => {
|
|||||||
lastScrollTop.value = 0
|
lastScrollTop.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击一级分类
|
|
||||||
const onParentCategoryClick = async (category: Category): Promise<void> => {
|
|
||||||
// 如果已经选中,则切换显示/隐藏二级分类
|
|
||||||
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
|
|
||||||
showSubCategories.value = !showSubCategories.value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中新的分类
|
|
||||||
selectedParentCategory.value = category
|
|
||||||
showSubCategories.value = true
|
|
||||||
|
|
||||||
// 加载二级分类
|
|
||||||
await loadSubCategories(category.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击二级分类
|
|
||||||
const onSubCategoryClick = (category: Category): void => {
|
|
||||||
// 跳转到分类页面
|
|
||||||
uni.setStorageSync('selectedCategory', category.id)
|
|
||||||
const timestamp = Date.now()
|
|
||||||
const randomParam = Math.random().toString(36).substring(2, 8)
|
|
||||||
const url = `/pages/mall/consumer/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}×tamp=${timestamp}&random=${randomParam}`
|
|
||||||
|
|
||||||
uni.switchTab({
|
|
||||||
url: '/pages/mall/consumer/category'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换分类 - 跳转到分类页面并传递分类ID
|
// 切换分类 - 跳转到分类页面并传递分类ID
|
||||||
const switchCategory = (category: any) => {
|
const switchCategory = (category: any) => {
|
||||||
console.log('=== switchCategory函数开始执行 ===')
|
console.log('=== switchCategory函数开始执行 ===')
|
||||||
@@ -807,35 +810,53 @@ const loadMore = async () => {
|
|||||||
|
|
||||||
// 添加到购物车
|
// 添加到购物车
|
||||||
const addToCart = async (product: any) => {
|
const addToCart = async (product: any) => {
|
||||||
uni.showLoading({ title: '添加中...' })
|
uni.showLoading({ title: '检查商品...' })
|
||||||
try {
|
try {
|
||||||
// 将 product 转换为 UTSJSONObject 以访问属性
|
// 将 product 转换为 UTSJSONObject 以访问属性
|
||||||
const prodObj = (product instanceof UTSJSONObject) ? (product as UTSJSONObject) : (JSON.parse(JSON.stringify(product)) as UTSJSONObject)
|
const prodObj = (product instanceof UTSJSONObject) ? (product as UTSJSONObject) : (JSON.parse(JSON.stringify(product)) as UTSJSONObject)
|
||||||
const productId = prodObj.getString('id') ?? ''
|
const productId = prodObj.getString('id') ?? ''
|
||||||
// 尝试调用 Supabase 服务添加
|
const merchantId = prodObj.getString('merchant_id') ?? ''
|
||||||
const success = await supabaseService.addToCart(productId, 1, '')
|
|
||||||
if (success) {
|
// 检查商品是否有SKU
|
||||||
|
const skus = await supabaseService.getProductSkus(productId)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '请选择规格',
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 失败(如未登录),回退到本地存储或提示登录
|
|
||||||
// 这里简单提示失败
|
|
||||||
uni.showToast({
|
|
||||||
title: '添加失败,请先登录',
|
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '添加失败,请先登录',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('添加到购物车异常', e)
|
console.error('添加到购物车异常', e)
|
||||||
uni.showToast({
|
uni.hideLoading()
|
||||||
title: '操作异常',
|
uni.showToast({
|
||||||
icon: 'none'
|
title: '操作异常',
|
||||||
})
|
icon: 'none'
|
||||||
} finally {
|
})
|
||||||
uni.hideLoading()
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扫码功能
|
// 扫码功能
|
||||||
@@ -1189,14 +1210,14 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-card {
|
.category-card {
|
||||||
width: 47%; /* 50 - 3 */
|
width: 18%; /* 一行5个 */
|
||||||
margin: 0 1.5% 16px 1.5%;
|
margin: 0 1% 12px 1%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 10px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
/* cursor: pointer; removed for uniapp-x support */
|
/* cursor: pointer; removed for uniapp-x support */
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -1282,27 +1303,31 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-icon {
|
.card-icon {
|
||||||
width: 56px;
|
width: 44px;
|
||||||
height: 56px;
|
height: 44px;
|
||||||
border-radius: 28px;
|
border-radius: 22px;
|
||||||
background: var(--card-color, #4CAF50);
|
background: var(--card-color, #4CAF50);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-icon-text {
|
.card-icon-text {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-name {
|
.card-name {
|
||||||
font-size: 15px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
lines: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-desc {
|
.card-desc {
|
||||||
@@ -1311,6 +1336,78 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 二级分类样式 */
|
||||||
|
.sub-category-grid {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-close {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #999;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-card {
|
||||||
|
width: 23%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
margin-right: 2%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-card .card-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-card .card-icon-text {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-category-card .card-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
lines: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* 健康资讯 */
|
/* 健康资讯 */
|
||||||
.health-news {
|
.health-news {
|
||||||
background: white;
|
background: white;
|
||||||
@@ -1513,125 +1610,62 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-card {
|
.product-card {
|
||||||
/* break-inside: avoid; removed for flex layout */
|
|
||||||
width: 48%; /* Fallback for calc(50% - 5px) */
|
|
||||||
/* margin-right: 2%; Gap handled by space-between */
|
|
||||||
margin-bottom: 20px; /* 增加底部间距 */
|
|
||||||
background: #ffffff; /* 改为纯白 */
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
/* cursor: pointer; removed for uvue support */
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid #eee; /* 更淡一点的边框 */
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
.product-card:hover {
|
overflow: hidden;
|
||||||
transform: translateY(-4px);
|
width: 48%;
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); /* 增强悬停阴影 */
|
margin-bottom: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.product-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
background: #FF5722;
|
|
||||||
color: white;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 180px; /* 默认稍微高一点 */
|
height: 170px;
|
||||||
background: #f8f9fa;
|
border-radius: 8px;
|
||||||
}
|
margin-bottom: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
.product-info {
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1; /* 撑开剩余空间 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 5px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
/* display: flex; removed for uniapp-x text support */
|
height: 36px;
|
||||||
/* overflow: hidden; */
|
overflow: hidden;
|
||||||
/* text-overflow: ellipsis; */
|
text-overflow: ellipsis;
|
||||||
/* display: -webkit-box; */
|
padding: 0 8px;
|
||||||
/* -webkit-line-clamp: 2; */
|
|
||||||
/* -webkit-box-orient: vertical; */
|
|
||||||
/* Simplified for compatibility */
|
|
||||||
display: flex;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-spec {
|
.product-bottom {
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
/* gap: 8px; removed */
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-price {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 8px; /* Replacement for gap */
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-symbol {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #FF5722;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #FF5722;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.original-price {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #999;
|
|
||||||
/* text-decoration: line-through; removed for uniapp-x support */
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-meta {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
padding: 0 8px 8px;
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.manufacturer {
|
.product-price {
|
||||||
color: #666;
|
font-size: 15px;
|
||||||
|
color: #ff5000;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sales-count {
|
.product-add-btn {
|
||||||
color: #999;
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #ff5000;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-action {
|
.add-icon {
|
||||||
margin-top: 12px;
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-btn {
|
.cart-btn {
|
||||||
|
|||||||
@@ -674,7 +674,7 @@ const addToCart = async (product: any) => {
|
|||||||
uni.showLoading({ title: '添加中...' })
|
uni.showLoading({ title: '添加中...' })
|
||||||
try {
|
try {
|
||||||
// 尝试调用 Supabase 服务添加
|
// 尝试调用 Supabase 服务添加
|
||||||
const success = await supabaseService.addToCart(product.id, 1, '')
|
const success = await supabaseService.addToCart(product.id, 1, '', '')
|
||||||
if (success) {
|
if (success) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '已添加到购物车',
|
||||||
|
|||||||
@@ -55,11 +55,7 @@ const totalPoints = ref<number>(0)
|
|||||||
const records = ref<PointRecord[]>([])
|
const records = ref<PointRecord[]>([])
|
||||||
const loading = ref<boolean>(true)
|
const loading = ref<boolean>(true)
|
||||||
|
|
||||||
const loadPoints = async () => {
|
const loadPoints = async (): Promise<void> => {
|
||||||
// 调用 service 获取积分 (需要supabaseService支持)
|
|
||||||
// 暂时如果service没更新,先用mock
|
|
||||||
// const res = await supabaseService.getUserPoints()
|
|
||||||
// if (res != null) totalPoints.value = res
|
|
||||||
try {
|
try {
|
||||||
const points = await supabaseService.getUserPoints()
|
const points = await supabaseService.getUserPoints()
|
||||||
totalPoints.value = points
|
totalPoints.value = points
|
||||||
@@ -68,26 +64,19 @@ const loadPoints = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadRecords = async () => {
|
const loadRecords = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const list = await supabaseService.getPointRecords()
|
const list = await supabaseService.getPointRecords()
|
||||||
const typedList: PointRecord[] = []
|
records.value = list as PointRecord[]
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
const item = list[i] as PointRecord
|
|
||||||
typedList.push(item)
|
|
||||||
}
|
|
||||||
records.value = typedList
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取积分记录失败', e)
|
console.error('获取积分记录失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async (): Promise<void> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await Promise.all<void>([
|
await loadPoints()
|
||||||
loadPoints(),
|
await loadRecords()
|
||||||
loadRecords()
|
|
||||||
])
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +92,20 @@ const handleExchange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTypeText = (type: string): string => {
|
const getTypeText = (type: string): string => {
|
||||||
if (type === 'signin') return '每日签到'
|
// 不支持 Record<string, string>,使用 if-else
|
||||||
if (type === 'shopping') return '购物奖励'
|
if (type == 'signin') {
|
||||||
if (type === 'redeem') return '积分兑换'
|
return '每日签到'
|
||||||
if (type === 'admin') return '系统调整'
|
} else if (type == 'shopping') {
|
||||||
if (type === 'register') return '注册赠送'
|
return '购物奖励'
|
||||||
return '积分变动'
|
} else if (type == 'redeem') {
|
||||||
|
return '积分兑换'
|
||||||
|
} else if (type == 'admin') {
|
||||||
|
return '系统调整'
|
||||||
|
} else if (type == 'register') {
|
||||||
|
return '注册赠送'
|
||||||
|
} else {
|
||||||
|
return '积分变动'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (timeStr: string): string => {
|
const formatTime = (timeStr: string): string => {
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export default {
|
|||||||
showSpec: false,
|
showSpec: false,
|
||||||
selectedSkuId: '',
|
selectedSkuId: '',
|
||||||
selectedSpec: '',
|
selectedSpec: '',
|
||||||
quantity: 1,
|
quantity: 1 as number,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
showParams: false,
|
showParams: false,
|
||||||
// 新增: 优惠券相关
|
// 新增: 优惠券相关
|
||||||
@@ -348,15 +348,21 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 移除已存在的相同商品(为了将其移到最新位置)
|
// 移除已存在的相同商品(为了将其移到最新位置)
|
||||||
footprints = footprints.filter(item => item.id !== productId)
|
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({
|
footprints.unshift({
|
||||||
id: this.product.id,
|
id: this.product.id,
|
||||||
name: this.product.name,
|
name: this.product.name,
|
||||||
price: this.product.price,
|
price: this.product.price,
|
||||||
original_price: this.product.original_price, // 添加原价
|
original_price: this.product.original_price,
|
||||||
image: this.product.images[0],
|
image: productImage,
|
||||||
sales: this.product.sales,
|
sales: this.product.sales,
|
||||||
shopId: this.merchant.id,
|
shopId: this.merchant.id,
|
||||||
shopName: this.merchant.shop_name,
|
shopName: this.merchant.shop_name,
|
||||||
@@ -386,6 +392,7 @@ export default {
|
|||||||
|
|
||||||
if (dbProduct != null) {
|
if (dbProduct != null) {
|
||||||
// Map DB product to local product
|
// Map DB product to local product
|
||||||
|
const dbObj = dbProduct as UTSJSONObject
|
||||||
this.product = {
|
this.product = {
|
||||||
id: dbProduct['id'] as string,
|
id: dbProduct['id'] as string,
|
||||||
merchant_id: (dbProduct['merchant_id'] ?? dbProduct['shop_id'] ?? '') as string,
|
merchant_id: (dbProduct['merchant_id'] ?? dbProduct['shop_id'] ?? '') as string,
|
||||||
@@ -453,7 +460,6 @@ export default {
|
|||||||
// Merge attributes into product if they match keys
|
// Merge attributes into product if they match keys
|
||||||
if (attrs['specification'] != null) this.product.specification = attrs['specification'] as string
|
if (attrs['specification'] != null) this.product.specification = attrs['specification'] as string
|
||||||
if (attrs['usage'] != null) this.product.usage = attrs['usage'] as string
|
if (attrs['usage'] != null) this.product.usage = attrs['usage'] as string
|
||||||
// ... augment as needed
|
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
@@ -686,7 +692,7 @@ export default {
|
|||||||
// 简化处理,直接返回 JSON 字符串
|
// 简化处理,直接返回 JSON 字符串
|
||||||
return JSON.stringify(specs)
|
return JSON.stringify(specs)
|
||||||
}
|
}
|
||||||
return sku.sku_code
|
return sku.sku_code ?? ''
|
||||||
},
|
},
|
||||||
|
|
||||||
async addToCart() {
|
async addToCart() {
|
||||||
@@ -704,7 +710,8 @@ export default {
|
|||||||
const success = await supabaseService.addToCart(
|
const success = await supabaseService.addToCart(
|
||||||
this.product.id,
|
this.product.id,
|
||||||
this.quantity,
|
this.quantity,
|
||||||
this.selectedSkuId
|
this.selectedSkuId,
|
||||||
|
this.product.merchant_id
|
||||||
)
|
)
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
|
||||||
@@ -740,7 +747,10 @@ export default {
|
|||||||
product_image: (sku != null && sku.image_url != null) ? sku!.image_url : this.product.images[0],
|
product_image: (sku != null && sku.image_url != null) ? sku!.image_url : this.product.images[0],
|
||||||
sku_specifications: sku != null ? sku!.specifications : {},
|
sku_specifications: sku != null ? sku!.specifications : {},
|
||||||
price: parseFloat((sku != null ? sku!.price : this.product.price).toString()).toFixed(2) as string,
|
price: parseFloat((sku != null ? sku!.price : this.product.price).toString()).toFixed(2) as string,
|
||||||
quantity: this.quantity as number
|
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_type', 'buy_now')
|
||||||
|
|||||||
@@ -491,13 +491,14 @@ export default {
|
|||||||
uAvatar = profile.getString('avatar_url') ?? ''
|
uAvatar = profile.getString('avatar_url') ?? ''
|
||||||
uGender = profile.getNumber('gender') ?? 0
|
uGender = profile.getNumber('gender') ?? 0
|
||||||
} else {
|
} else {
|
||||||
const profileObj = profile as UTSJSONObject
|
// 必须使用 JSON.parse(JSON.stringify()) 转换为 UTSJSONObject
|
||||||
uId = (profileObj.getString('user_id') ?? '') as string
|
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
|
||||||
uPhone = (profileObj.getString('phone') ?? '') as string
|
uId = profileObj.getString('user_id') ?? ''
|
||||||
uEmail = (profileObj.getString('email') ?? '') as string
|
uPhone = profileObj.getString('phone') ?? ''
|
||||||
uNickname = (profileObj.getString('nickname') ?? '') as string
|
uEmail = profileObj.getString('email') ?? ''
|
||||||
uAvatar = (profileObj.getString('avatar_url') ?? '') as string
|
uNickname = profileObj.getString('nickname') ?? ''
|
||||||
uGender = (profileObj.getNumber('gender') ?? 0) as number
|
uAvatar = profileObj.getString('avatar_url') ?? ''
|
||||||
|
uGender = profileObj.getNumber('gender') ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uNickname === '' && uPhone !== '') {
|
if (uNickname === '' && uPhone !== '') {
|
||||||
|
|||||||
@@ -119,15 +119,12 @@
|
|||||||
class="guess-item"
|
class="guess-item"
|
||||||
@click="viewProductDetail(item)"
|
@click="viewProductDetail(item)"
|
||||||
>
|
>
|
||||||
<view class="guess-img-box">
|
<image class="guess-img" :src="item.image" mode="aspectFill" />
|
||||||
<image class="guess-img" :src="item.image" mode="aspectFill" />
|
<text class="guess-name" :lines="2">{{ item.name }}</text>
|
||||||
</view>
|
<view class="guess-bottom">
|
||||||
<view class="guess-info">
|
<text class="guess-price">¥{{ item.price }}</text>
|
||||||
<text class="guess-name">{{ item.name }}</text>
|
<view class="guess-add-btn" @click.stop="addToCart(item)">
|
||||||
<view class="guess-price-row">
|
<text class="guess-add-icon">+</text>
|
||||||
<text class="price-symbol">¥</text>
|
|
||||||
<text class="price-num">{{ item.price }}</text>
|
|
||||||
<text class="sales-text">已售{{ item.sales }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -206,21 +203,11 @@
|
|||||||
@click="viewProductDetail(product)"
|
@click="viewProductDetail(product)"
|
||||||
>
|
>
|
||||||
<image class="product-image" :src="product.image" mode="aspectFill" />
|
<image class="product-image" :src="product.image" mode="aspectFill" />
|
||||||
<view class="product-info">
|
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||||
<text class="product-name">{{ product.name }}</text>
|
<view class="product-bottom">
|
||||||
<view class="product-tags-row" v-if="product.tag">
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
<text class="product-tag">{{ product.tag }}</text>
|
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||||
</view>
|
<text class="add-icon">+</text>
|
||||||
<text class="product-spec">{{ product.specification }}</text>
|
|
||||||
|
|
||||||
<view class="product-bottom">
|
|
||||||
<view class="price-box">
|
|
||||||
<text class="price-symbol">¥</text>
|
|
||||||
<text class="price-value">{{ product.price }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="add-cart-btn" @click.stop="addToCart(product)">
|
|
||||||
<text class="cart-icon">+</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -278,6 +265,7 @@ type GuessItemType = {
|
|||||||
price: number
|
price: number
|
||||||
image: string
|
image: string
|
||||||
sales: number
|
sales: number
|
||||||
|
merchant_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResultType = {
|
type SearchResultType = {
|
||||||
@@ -288,6 +276,7 @@ type SearchResultType = {
|
|||||||
specification: string
|
specification: string
|
||||||
tag: string
|
tag: string
|
||||||
sales: number
|
sales: number
|
||||||
|
merchant_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShopResultType = {
|
type ShopResultType = {
|
||||||
@@ -377,14 +366,23 @@ const loadData = async (): Promise<void> => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loadSearchHistory()
|
loadSearchHistory()
|
||||||
const hotProducts = await supabaseService.getHotProducts(30)
|
|
||||||
|
// 获取热销商品,失败时使用空数组
|
||||||
|
let hotProducts: Product[] = []
|
||||||
|
try {
|
||||||
|
const hotResult = await supabaseService.getHotProducts(30)
|
||||||
|
hotProducts = hotResult as Product[]
|
||||||
|
} catch (hotError) {
|
||||||
|
console.error('获取热销商品失败,使用空列表:', hotError)
|
||||||
|
hotProducts = []
|
||||||
|
}
|
||||||
|
|
||||||
const hotList: Array<HotSearchItemType> = []
|
const hotList: Array<HotSearchItemType> = []
|
||||||
const limit1 = hotProducts.length < 10 ? hotProducts.length : 10
|
const limit1 = hotProducts.length < 10 ? hotProducts.length : 10
|
||||||
for (let i: number = 0; i < limit1; i++) {
|
for (let i: number = 0; i < limit1; i++) {
|
||||||
const p = hotProducts[i] as UTSJSONObject
|
const p = hotProducts[i]
|
||||||
const item: HotSearchItemType = {
|
const item: HotSearchItemType = {
|
||||||
keyword: p.getString('name') ?? '',
|
keyword: p.name ?? '',
|
||||||
hot: true
|
hot: true
|
||||||
}
|
}
|
||||||
hotList.push(item)
|
hotList.push(item)
|
||||||
@@ -393,14 +391,15 @@ const loadData = async (): Promise<void> => {
|
|||||||
|
|
||||||
const allItems: Array<GuessItemType> = []
|
const allItems: Array<GuessItemType> = []
|
||||||
for (let i: number = 0; i < hotProducts.length; i++) {
|
for (let i: number = 0; i < hotProducts.length; i++) {
|
||||||
const p = hotProducts[i] as UTSJSONObject
|
const p = hotProducts[i]
|
||||||
const saleCount = p.getNumber('sale_count')
|
const saleCount = p.sale_count
|
||||||
const item: GuessItemType = {
|
const item: GuessItemType = {
|
||||||
id: p.getString('id') ?? '',
|
id: p.id ?? '',
|
||||||
name: p.getString('name') ?? '',
|
name: p.name ?? '',
|
||||||
price: p.getNumber('base_price') ?? 0,
|
price: p.base_price ?? 0,
|
||||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
image: p.main_image_url ?? '/static/default.jpg',
|
||||||
sales: saleCount != null ? saleCount : 0
|
sales: saleCount != null ? saleCount : 0,
|
||||||
|
merchant_id: p.merchant_id ?? ''
|
||||||
}
|
}
|
||||||
allItems.push(item)
|
allItems.push(item)
|
||||||
}
|
}
|
||||||
@@ -410,7 +409,8 @@ const loadData = async (): Promise<void> => {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Load data failed', e)
|
console.error('Load data failed', e)
|
||||||
isError.value = true
|
// 不再显示错误页面,允许使用空数据
|
||||||
|
isError.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,14 +517,14 @@ const performSearch = async (): Promise<void> => {
|
|||||||
const prodData = prodResp.data != null ? prodResp.data : []
|
const prodData = prodResp.data != null ? prodResp.data : []
|
||||||
const resultList: Array<SearchResultType> = []
|
const resultList: Array<SearchResultType> = []
|
||||||
for (let i: number = 0; i < prodData.length; i++) {
|
for (let i: number = 0; i < prodData.length; i++) {
|
||||||
const p = prodData[i] as UTSJSONObject
|
const p = prodData[i] as Product
|
||||||
let tag = ''
|
let tag = ''
|
||||||
const tagsRaw = p.get('tags')
|
const tagsRaw = p.tags
|
||||||
if (tagsRaw != null) {
|
if (tagsRaw != null) {
|
||||||
try {
|
try {
|
||||||
const tagsStr = p.getString('tags')
|
const tagsStr = p.tags
|
||||||
if (tagsStr != null) {
|
if (tagsStr != null) {
|
||||||
const tags = JSON.parse(tagsStr)
|
const tags = JSON.parse(tagsStr as string)
|
||||||
if (Array.isArray(tags) && tags.length > 0) {
|
if (Array.isArray(tags) && tags.length > 0) {
|
||||||
const firstTag = tags[0]
|
const firstTag = tags[0]
|
||||||
tag = firstTag != null ? (firstTag as string) : ''
|
tag = firstTag != null ? (firstTag as string) : ''
|
||||||
@@ -534,13 +534,14 @@ const performSearch = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchItem: SearchResultType = {
|
const searchItem: SearchResultType = {
|
||||||
id: p.getString('id') ?? '',
|
id: p.id ?? '',
|
||||||
name: p.getString('name') ?? '',
|
name: p.name ?? '',
|
||||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
image: p.main_image_url ?? '/static/default.jpg',
|
||||||
price: p.getNumber('base_price') ?? 0,
|
price: p.base_price ?? 0,
|
||||||
specification: p.getString('specification') ?? '标准规格',
|
specification: p.specification ?? '标准规格',
|
||||||
tag: tag,
|
tag: tag,
|
||||||
sales: p.getNumber('sale_count') ?? 0
|
sales: p.sale_count ?? 0,
|
||||||
|
merchant_id: p.merchant_id ?? ''
|
||||||
}
|
}
|
||||||
resultList.push(searchItem)
|
resultList.push(searchItem)
|
||||||
}
|
}
|
||||||
@@ -599,21 +600,47 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onInput = (e: any) => {
|
const onInput = (e: any) => {
|
||||||
const eObj = e as UTSJSONObject
|
try {
|
||||||
const detailRaw = eObj.get('detail')
|
let val = ''
|
||||||
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject())
|
// 处理 input 事件的不同事件对象格式
|
||||||
const val = detail.getString('value') ?? ''
|
if (e != null) {
|
||||||
searchKeyword.value = val
|
// UTSJSONObject 格式 (e.detail.value)
|
||||||
if (val == '') {
|
if (e instanceof UTSJSONObject) {
|
||||||
showResults.value = false
|
const eObj = e as UTSJSONObject
|
||||||
searchSuggestions.value = []
|
const detailObj = eObj.get('detail')
|
||||||
return
|
if (detailObj != null && detailObj instanceof UTSJSONObject) {
|
||||||
}
|
const detail = detailObj as UTSJSONObject
|
||||||
|
const v = detail.get('value')
|
||||||
|
val = v != null ? (v as string) : ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 尝试转换为 UTSJSONObject
|
||||||
|
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
|
||||||
|
const detailObj = eObj.get('detail')
|
||||||
|
if (detailObj != null) {
|
||||||
|
const detail = detailObj as UTSJSONObject
|
||||||
|
const v = detail.get('value')
|
||||||
|
val = v != null ? (v as string) : ''
|
||||||
|
} else {
|
||||||
|
const v = eObj.get('value')
|
||||||
|
val = v != null ? (v as string) : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchKeyword.value = val
|
||||||
|
if (val == '') {
|
||||||
|
showResults.value = false
|
||||||
|
searchSuggestions.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (suggestTimer > 0) clearTimeout(suggestTimer)
|
if (suggestTimer > 0) clearTimeout(suggestTimer)
|
||||||
suggestTimer = setTimeout(() => {
|
suggestTimer = setTimeout(() => {
|
||||||
fetchSuggestions(val)
|
fetchSuggestions(val)
|
||||||
}, 300)
|
}, 300)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('onInput error:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
@@ -702,7 +729,8 @@ const loadMore = async (): Promise<void> => {
|
|||||||
price: p.getNumber('base_price') ?? 0,
|
price: p.getNumber('base_price') ?? 0,
|
||||||
specification: p.getString('specification') ?? '标准规格',
|
specification: p.getString('specification') ?? '标准规格',
|
||||||
tag: tag,
|
tag: tag,
|
||||||
sales: p.getNumber('sale_count') ?? 0
|
sales: p.getNumber('sale_count') ?? 0,
|
||||||
|
merchant_id: p.getString('merchant_id') ?? ''
|
||||||
}
|
}
|
||||||
searchResults.value.push(searchItem)
|
searchResults.value.push(searchItem)
|
||||||
}
|
}
|
||||||
@@ -738,11 +766,42 @@ const viewShopDetail = (shop: ShopResultType) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToCart = (product: SearchResultType | GuessItemType) => {
|
const addToCart = async (product: SearchResultType | GuessItemType) => {
|
||||||
uni.showToast({ title: '请选择规格', icon: 'none' })
|
uni.showLoading({ title: '检查商品...' })
|
||||||
setTimeout(() => {
|
try {
|
||||||
viewProductDetail(product)
|
// 统一转换为 UTSJSONObject 访问属性
|
||||||
}, 800)
|
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
|
||||||
|
const productId = prodObj.getString('id') ?? ''
|
||||||
|
const merchantId = prodObj.getString('merchant_id') ?? ''
|
||||||
|
|
||||||
|
// 检查商品是否有SKU
|
||||||
|
const skus = await supabaseService.getProductSkus(productId)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
|
uni.showToast({ title: '请选择规格', icon: 'none' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('添加到购物车异常', e)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '操作异常', icon: 'none' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCamera = () => {
|
const openCamera = () => {
|
||||||
@@ -1142,64 +1201,62 @@ const goBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guess-item {
|
.guess-item {
|
||||||
background-color: #fff;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 8px;
|
|
||||||
width: 48%;
|
width: 48%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.guess-img-box {
|
|
||||||
width: 100%;
|
|
||||||
height: 0;
|
|
||||||
padding-bottom: 100%;
|
|
||||||
position: relative;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guess-img {
|
.guess-img {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 170px;
|
||||||
}
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
.guess-info {
|
background: #f5f5f5;
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guess-name {
|
.guess-name {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 5px;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
line-height: 1.3;
|
padding: 0 8px;
|
||||||
height: 34px; /* 限制2行高度 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guess-price-row {
|
.guess-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end; /* REPLACED baseline */
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-symbol {
|
.guess-price {
|
||||||
font-size: 12px;
|
font-size: 15px;
|
||||||
color: #ff5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-num {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #ff5000;
|
color: #ff5000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sales-text {
|
.guess-add-btn {
|
||||||
font-size: 10px;
|
width: 24px;
|
||||||
color: #999;
|
height: 24px;
|
||||||
|
background-color: #ff5000;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guess-add-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 搜索建议列表 */
|
/* 搜索建议列表 */
|
||||||
@@ -1312,84 +1369,61 @@ const goBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-item {
|
.result-item {
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* 垂直排列 */
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
width: 48%;
|
width: 48%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 120px; /* 调整图片高度 */
|
height: 170px;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
background-color: #f0f0f0;
|
margin-bottom: 8px;
|
||||||
}
|
background: #f5f5f5;
|
||||||
|
|
||||||
.product-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 13px; /* 减小字号 */
|
font-size: 13px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: bold;
|
margin-bottom: 5px;
|
||||||
line-height: 1.3;
|
line-height: 1.4;
|
||||||
height: 34px; /* 限制高度 */
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
padding: 0 8px;
|
||||||
|
|
||||||
.product-tags-row {
|
|
||||||
margin-top: 2px;
|
|
||||||
display: none; /* 隐藏标签以保持简洁 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-spec {
|
|
||||||
display: none; /* 隐藏规格 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-bottom {
|
.product-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center; /* 垂直居中 */
|
align-items: center;
|
||||||
margin-top: 4px;
|
padding: 0 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-box {
|
.product-price {
|
||||||
|
font-size: 15px;
|
||||||
color: #ff5000;
|
color: #ff5000;
|
||||||
display: flex;
|
font-weight: bold;
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-symbol {
|
.product-add-btn {
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
font-size: 16px; /* 减小价格字号 */
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-cart-btn {
|
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background-color: #4CAF50;
|
background-color: #ff5000;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-icon {
|
.add-icon {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,16 +49,11 @@
|
|||||||
<view class="product-grid">
|
<view class="product-grid">
|
||||||
<view v-for="product in products" :key="product.id" class="product-item" @click="goToProduct(product.id)">
|
<view v-for="product in products" :key="product.id" class="product-item" @click="goToProduct(product.id)">
|
||||||
<image :src="product.images[0]" class="product-image" mode="aspectFill" />
|
<image :src="product.images[0]" class="product-image" mode="aspectFill" />
|
||||||
<view class="product-info">
|
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||||
<text class="product-name">{{ product.name }}</text>
|
<view class="product-bottom">
|
||||||
<view class="price-row">
|
<text class="product-price">¥{{ product.price }}</text>
|
||||||
<view class="price-left">
|
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||||
<text class="product-price">¥{{ product.price }}</text>
|
<text class="add-icon">+</text>
|
||||||
<text class="product-sales">已售 {{ product.sales }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="cart-btn" @click.stop="addToCart(product)">
|
|
||||||
<text class="cart-icon">🛒</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -70,9 +65,20 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { MerchantType, ProductType, CouponTemplateType } from '@/types/mall-types.uts'
|
import { MerchantType, ProductType } from '@/types/mall-types.uts'
|
||||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||||
|
|
||||||
|
// 优惠券类型定义
|
||||||
|
type CouponType = {
|
||||||
|
id: string
|
||||||
|
discount_value: number
|
||||||
|
min_order_amount: number
|
||||||
|
name: string
|
||||||
|
start_time: string
|
||||||
|
end_time: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
|
|
||||||
// 分页相关状态
|
// 分页相关状态
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(6) // 默认显示六个
|
const pageSize = ref(6) // 默认显示六个
|
||||||
@@ -95,49 +101,50 @@ const merchant = ref<MerchantType>({
|
|||||||
created_at: ''
|
created_at: ''
|
||||||
} as MerchantType)
|
} as MerchantType)
|
||||||
|
|
||||||
const products = ref<Array<ProductType>>([])
|
const products = ref<ProductType[]>([])
|
||||||
const isFollowed = ref<boolean>(false)
|
const isFollowed = ref(false)
|
||||||
const coupons = ref<Array<CouponTemplateType>>([])
|
const coupons = ref<CouponType[]>([]) // 新增优惠券
|
||||||
const isRefresherTriggered = ref<boolean>(false)
|
const isRefresherTriggered = ref(false)
|
||||||
|
|
||||||
const checkFollowStatus = async (shopId: string): Promise<void> => {
|
// 函数定义必须在 onMounted 之前
|
||||||
|
// checkFollowStatus 必须在 loadShopData 之前定义
|
||||||
|
const checkFollowStatus = async (shopId: string) => {
|
||||||
const userId = supabaseService.getCurrentUserId()
|
const userId = supabaseService.getCurrentUserId()
|
||||||
if (userId != null && userId !== '') {
|
if (userId != null && userId != '') {
|
||||||
try {
|
try {
|
||||||
|
// @ts-ignore
|
||||||
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
|
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.warn('isShopFollowed method not available')
|
console.warn('isShopFollowed method not found')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadShopData = async (id: string): Promise<void> => {
|
const loadShopData = async (id: string) => {
|
||||||
console.log('Loading shop data for:', id)
|
console.log('Loading shop data for:', id)
|
||||||
const shop = await supabaseService.getShopByMerchantId(id)
|
const shop = await supabaseService.getShopByMerchantId(id)
|
||||||
|
|
||||||
if (shop != null) {
|
if (shop != null) {
|
||||||
console.log('Shop loaded successfully:', shop.shop_name)
|
console.log('Shop loaded successfully:', shop.shop_name)
|
||||||
const shopObj = shop as UTSJSONObject
|
// 使用显式类型转换
|
||||||
const merchantData: MerchantType = {
|
const merchantData: MerchantType = {
|
||||||
id: shopObj.getString('id') ?? '',
|
id: shop.id,
|
||||||
user_id: shopObj.getString('merchant_id') ?? '',
|
user_id: shop.merchant_id,
|
||||||
shop_name: shopObj.getString('shop_name') ?? '',
|
shop_name: shop.shop_name,
|
||||||
shop_logo: shopObj.getString('shop_logo') ?? '/static/default-shop.png',
|
shop_logo: shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png',
|
||||||
shop_banner: shopObj.getString('shop_banner') ?? '/static/default-banner.png',
|
shop_banner: shop.shop_banner != null ? shop.shop_banner : '/static/default-banner.png',
|
||||||
shop_description: shopObj.getString('description') ?? '',
|
shop_description: shop.description != null ? shop.description : '',
|
||||||
contact_name: shopObj.getString('contact_name') ?? '',
|
contact_name: shop.contact_name != null ? shop.contact_name : '',
|
||||||
contact_phone: shopObj.getString('contact_phone') ?? '',
|
contact_phone: shop.contact_phone != null ? shop.contact_phone : '',
|
||||||
shop_status: 1,
|
shop_status: 1,
|
||||||
rating: shopObj.getNumber('rating_avg') ?? 5.0,
|
rating: shop.rating_avg != null ? shop.rating_avg : 5.0,
|
||||||
total_sales: shopObj.getNumber('total_sales') ?? 0,
|
total_sales: shop.total_sales != null ? shop.total_sales : 0,
|
||||||
created_at: shopObj.getString('created_at') ?? ''
|
created_at: shop.created_at != null ? shop.created_at : ''
|
||||||
} as MerchantType
|
}
|
||||||
merchant.value = merchantData
|
merchant.value = merchantData
|
||||||
|
|
||||||
const shopId = shopObj.getString('id') ?? ''
|
// 检查关注状态
|
||||||
if (shopId !== '') {
|
checkFollowStatus(shop.id)
|
||||||
checkFollowStatus(shopId)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Shop data is null for ID:', id)
|
console.warn('Shop data is null for ID:', id)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -148,47 +155,37 @@ const loadShopData = async (id: string): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadCoupons = async (id: string): Promise<void> => {
|
const loadCoupons = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const rawCoupons = await supabaseService.fetchShopCoupons(id)
|
// @ts-ignore
|
||||||
if (rawCoupons != null && Array.isArray(rawCoupons)) {
|
const res = await supabaseService.fetchShopCoupons(id)
|
||||||
const couponList: Array<CouponTemplateType> = []
|
if (res != null && Array.isArray(res)) {
|
||||||
for (let i: number = 0; i < rawCoupons.length; i++) {
|
const couponList: CouponType[] = []
|
||||||
const c = rawCoupons[i] as UTSJSONObject
|
for (let i = 0; i < res.length; i++) {
|
||||||
const coupon: CouponTemplateType = {
|
const item = res[i]
|
||||||
id: c.getString('id') ?? '',
|
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
name: c.getString('name') ?? '',
|
couponList.push({
|
||||||
description: c.getString('description'),
|
id: itemObj.getString('id') ?? '',
|
||||||
coupon_type: c.getNumber('coupon_type') ?? 0,
|
discount_value: itemObj.getNumber('discount_value') ?? 0,
|
||||||
discount_type: c.getNumber('discount_type') ?? 0,
|
min_order_amount: itemObj.getNumber('min_order_amount') ?? 0,
|
||||||
discount_value: c.getNumber('discount_value') ?? 0,
|
name: itemObj.getString('name') ?? '',
|
||||||
min_order_amount: c.getNumber('min_order_amount') ?? 0,
|
start_time: itemObj.getString('start_time') ?? '',
|
||||||
max_discount_amount: c.getNumber('max_discount_amount'),
|
end_time: itemObj.getString('end_time') ?? '',
|
||||||
total_quantity: c.getNumber('total_quantity'),
|
status: itemObj.getNumber('status') ?? 1
|
||||||
per_user_limit: c.getNumber('per_user_limit') ?? 1,
|
} as CouponType)
|
||||||
usage_limit: c.getNumber('usage_limit') ?? 0,
|
|
||||||
merchant_id: c.getString('merchant_id'),
|
|
||||||
category_ids: [],
|
|
||||||
product_ids: [],
|
|
||||||
user_type_limit: c.getNumber('user_type_limit'),
|
|
||||||
start_time: c.getString('start_time') ?? '',
|
|
||||||
end_time: c.getString('end_time') ?? '',
|
|
||||||
status: c.getNumber('status') ?? 1,
|
|
||||||
created_at: c.getString('created_at') ?? ''
|
|
||||||
} as CouponTemplateType
|
|
||||||
couponList.push(coupon)
|
|
||||||
}
|
}
|
||||||
coupons.value = couponList
|
coupons.value = couponList
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e1) {
|
||||||
console.warn('SupabaseService.fetchShopCoupons method missing.')
|
console.warn('SupabaseService.fetchShopCoupons method missing. Please rebuild project.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadShopProducts = async (id: string): Promise<void> => {
|
const loadShopProducts = async (id: string) => {
|
||||||
if (isLoading.value) return
|
if (isLoading.value) return
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
|
// 保存当前使用的MerchantID,供下拉/触底使用
|
||||||
if (currentPage.value === 1) {
|
if (currentPage.value === 1) {
|
||||||
currentMerchantId.value = id
|
currentMerchantId.value = id
|
||||||
}
|
}
|
||||||
@@ -197,50 +194,51 @@ const loadShopProducts = async (id: string): Promise<void> => {
|
|||||||
|
|
||||||
let res: any = {}
|
let res: any = {}
|
||||||
try {
|
try {
|
||||||
|
// @ts-ignore
|
||||||
res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
|
res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('getProductsByMerchantId missing or error:', e)
|
console.error('getProductsByMerchantId missing or error', e)
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawList = res?.data
|
console.log(`shop-detail getProductsByMerchantId result count: ${res.data?.length}`)
|
||||||
|
|
||||||
|
const rawList = res.data
|
||||||
if (rawList != null && Array.isArray(rawList) && rawList.length > 0) {
|
if (rawList != null && Array.isArray(rawList) && rawList.length > 0) {
|
||||||
const list: Array<ProductType> = []
|
const list = rawList.map((item: any): ProductType => {
|
||||||
|
// 解析图片数组
|
||||||
|
let images: string[] = []
|
||||||
|
|
||||||
for (let idx: number = 0; idx < rawList.length; idx++) {
|
// 转换为 UTSJSONObject 安全访问属性
|
||||||
const item = rawList[idx] as UTSJSONObject
|
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||||
const images: Array<string> = []
|
|
||||||
|
|
||||||
const mainImageUrl = item.getString('main_image_url')
|
// 1. 尝试 main_image_url
|
||||||
if (mainImageUrl != null && mainImageUrl !== '') {
|
const mainImageUrl = itemObj.getString('main_image_url')
|
||||||
|
if (mainImageUrl != null && mainImageUrl != '') {
|
||||||
images.push(mainImageUrl)
|
images.push(mainImageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrlsRaw = item.get('image_urls')
|
// 2. 尝试 image_urls (如果 main 为空,或者需要展示多图)
|
||||||
if (imageUrlsRaw != null) {
|
const imageUrls = itemObj.get('image_urls')
|
||||||
|
if (imageUrls != null) {
|
||||||
try {
|
try {
|
||||||
if (Array.isArray(imageUrlsRaw)) {
|
if (Array.isArray(imageUrls)) {
|
||||||
const arr = imageUrlsRaw as Array<string>
|
const arr = imageUrls as string[]
|
||||||
if (arr.length > 0) {
|
if (arr.length > 0) {
|
||||||
if (images.length == 0) {
|
if (images.length == 0) images.push(...arr)
|
||||||
for (let i: number = 0; i < arr.length; i++) {
|
|
||||||
images.push(arr[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else if (typeof imageUrls === 'string') {
|
||||||
const rawUrlStr = imageUrlsRaw as string
|
const rawUrl = imageUrls as string
|
||||||
if (rawUrlStr.startsWith('[')) {
|
if (rawUrl.startsWith('[')) {
|
||||||
const parsed = JSON.parse(rawUrlStr)
|
const parsed = JSON.parse(rawUrl)
|
||||||
if (Array.isArray(parsed) && images.length == 0) {
|
if (Array.isArray(parsed)) {
|
||||||
for (let i: number = 0; i < parsed.length; i++) {
|
const arr = parsed as string[]
|
||||||
images.push(parsed[i] as string)
|
if (images.length == 0) images.push(...arr)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (images.indexOf(rawUrlStr) === -1) images.push(rawUrlStr)
|
if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -248,84 +246,146 @@ const loadShopProducts = async (id: string): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 没有任何图片则使用默认
|
||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
images.push('/static/default-product.png')
|
images.push('/static/default-product.png')
|
||||||
}
|
}
|
||||||
|
|
||||||
let safePrice = item.getNumber('base_price')
|
return {
|
||||||
if (safePrice == null) {
|
id: itemObj.getString('id') ?? '',
|
||||||
const p = item.getNumber('price')
|
merchant_id: itemObj.getString('merchant_id') ?? '',
|
||||||
safePrice = p != null ? p : 0
|
category_id: itemObj.getString('category_id') ?? '',
|
||||||
}
|
name: itemObj.getString('name') ?? '未知商品',
|
||||||
|
description: itemObj.getString('description') ?? '',
|
||||||
let safeMarketPrice = item.getNumber('market_price')
|
|
||||||
if (safeMarketPrice == null) {
|
|
||||||
const mp = item.getNumber('original_price')
|
|
||||||
safeMarketPrice = mp != null ? mp : safePrice
|
|
||||||
}
|
|
||||||
|
|
||||||
let safeStock = item.getNumber('total_stock')
|
|
||||||
if (safeStock == null) {
|
|
||||||
let as_ = item.getNumber('available_stock')
|
|
||||||
if (as_ == null) {
|
|
||||||
const s = item.getNumber('stock')
|
|
||||||
safeStock = s != null ? s : 0
|
|
||||||
} else {
|
|
||||||
safeStock = as_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let safeSales = item.getNumber('sale_count')
|
|
||||||
if (safeSales == null) {
|
|
||||||
const s = item.getNumber('sales')
|
|
||||||
safeSales = s != null ? s : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const product: ProductType = {
|
|
||||||
id: item.getString('id') ?? '',
|
|
||||||
merchant_id: item.getString('merchant_id') ?? '',
|
|
||||||
category_id: item.getString('category_id') ?? '',
|
|
||||||
name: item.getString('name') ?? '',
|
|
||||||
description: item.getString('description') ?? '',
|
|
||||||
images: images,
|
images: images,
|
||||||
price: safePrice,
|
price: itemObj.getNumber('base_price') ?? 0,
|
||||||
original_price: safeMarketPrice,
|
original_price: itemObj.getNumber('market_price') ?? 0,
|
||||||
stock: safeStock,
|
stock: itemObj.getNumber('total_stock') ?? 0,
|
||||||
sales: safeSales,
|
sales: itemObj.getNumber('sale_count') ?? 0,
|
||||||
status: 1,
|
status: 1,
|
||||||
created_at: item.getString('created_at') ?? ''
|
created_at: itemObj.getString('created_at') ?? ''
|
||||||
} as ProductType
|
} as ProductType
|
||||||
list.push(product)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPage.value === 1) {
|
if (currentPage.value === 1) {
|
||||||
products.value = list
|
products.value = list
|
||||||
} else {
|
} else {
|
||||||
for (let i: number = 0; i < list.length; i++) {
|
products.value.push(...list)
|
||||||
products.value.push(list[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否还有更多
|
|
||||||
if (list.length < pageSize.value) {
|
|
||||||
hasMore.value = false
|
|
||||||
} else {
|
|
||||||
hasMore.value = true
|
|
||||||
currentPage.value++ // 准备下一页
|
|
||||||
}
|
}
|
||||||
|
currentPage.value++
|
||||||
|
hasMore.value = list.length >= pageSize.value
|
||||||
} else {
|
} else {
|
||||||
console.log('未加载到店铺商品 (本页为空)')
|
hasMore.value = false
|
||||||
if (currentPage.value === 1) {
|
|
||||||
products.value = []
|
|
||||||
}
|
|
||||||
hasMore.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleFollow = async (): Promise<void> => {
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const options = pages[pages.length - 1].options as UTSJSONObject
|
||||||
|
// Search传递的是 id (shop_id), 其他地方可能传递 merchantId
|
||||||
|
const mId = options.get('merchantId')
|
||||||
|
const pId = options.get('id')
|
||||||
|
const paramId = (mId != null ? mId : pId) as string
|
||||||
|
|
||||||
|
if (paramId != null && paramId != '') {
|
||||||
|
console.log('Page mounted with params:', paramId)
|
||||||
|
// 优先加载店铺信息
|
||||||
|
loadShopData(paramId).then(() => {
|
||||||
|
// 加载成功后,使用确定的 merchant_id 来查询关联数据 (商品/优惠券通常是关联在 merchant_id 上的)
|
||||||
|
const realMerchantId = merchant.value.user_id // 这里 user_id 映射了 DB 中的 merchant_id
|
||||||
|
if (realMerchantId != null && realMerchantId != '') {
|
||||||
|
console.log('Chain loading products for Corrected Merchant ID:', realMerchantId)
|
||||||
|
currentMerchantId.value = realMerchantId // 更新当前上下文ID
|
||||||
|
loadShopProducts(realMerchantId)
|
||||||
|
loadCoupons(realMerchantId)
|
||||||
|
} else {
|
||||||
|
// 防御性策略:如果没能获取 merchant_id,尝试用传入 ID
|
||||||
|
console.warn('Shop load failed or id empty, fallback using original id:', paramId)
|
||||||
|
currentMerchantId.value = paramId
|
||||||
|
loadShopProducts(paramId)
|
||||||
|
loadCoupons(paramId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.error('No ID passed to shop-detail')
|
||||||
|
uni.showToast({title: '参数错误', icon: 'error'})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onRefresherRefresh = () => {
|
||||||
|
isRefresherTriggered.value = true
|
||||||
|
currentPage.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
isLoading.value = false
|
||||||
|
|
||||||
|
if (currentMerchantId.value != '') {
|
||||||
|
const id = currentMerchantId.value
|
||||||
|
Promise.all([
|
||||||
|
loadShopData(id),
|
||||||
|
loadCoupons(id),
|
||||||
|
loadShopProducts(id)
|
||||||
|
]).then(() => {
|
||||||
|
isRefresherTriggered.value = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
isRefresherTriggered.value = false
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onScrollToLower = () => {
|
||||||
|
if (hasMore.value && !isLoading.value && currentMerchantId.value != '') {
|
||||||
|
console.log('Scroll to lower, loading more...')
|
||||||
|
loadShopProducts(currentMerchantId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPullDownRefresh(() => {
|
||||||
|
onRefresherRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
onScrollToLower()
|
||||||
|
})
|
||||||
|
|
||||||
|
const claimCoupon = async (coupon: any) => {
|
||||||
|
const userId = supabaseService.getCurrentUserId()
|
||||||
|
if (userId == null) {
|
||||||
|
uni.navigateTo({ url: '/pages/auth/login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '领取中' })
|
||||||
|
|
||||||
|
// 转换为 UTSJSONObject 安全访问属性
|
||||||
|
const couponObj = JSON.parse(JSON.stringify(coupon)) as UTSJSONObject
|
||||||
|
const couponId = couponObj.getString('id') ?? ''
|
||||||
|
|
||||||
|
let success = false
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
success = await supabaseService.claimShopCoupon(couponId, userId)
|
||||||
|
} catch(e1) {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
success = await supabaseService.claimCoupon(couponId, userId)
|
||||||
|
} catch(e2) {
|
||||||
|
console.warn('claimCoupon not found')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.hideLoading()
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({ title: '领取成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '领取失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFollow = async () => {
|
||||||
const userId = supabaseService.getCurrentUserId()
|
const userId = supabaseService.getCurrentUserId()
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
uni.navigateTo({ url: '/pages/auth/login' })
|
uni.navigateTo({ url: '/pages/auth/login' })
|
||||||
@@ -338,32 +398,25 @@ const toggleFollow = async (): Promise<void> => {
|
|||||||
|
|
||||||
uni.showLoading({ title: '处理中' })
|
uni.showLoading({ title: '处理中' })
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
if (isFollowed.value) {
|
if (isFollowed.value) {
|
||||||
// 取消关注
|
// 取消关注
|
||||||
try {
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const success = await supabaseService.unfollowShop(shopId, userId)
|
||||||
const success = await supabaseService.unfollowShop(shopId, userId)
|
if (success) {
|
||||||
if (success) {
|
isFollowed.value = false
|
||||||
isFollowed.value = false
|
uni.showToast({ title: '已取消关注', icon: 'none' })
|
||||||
uni.showToast({ title: '已取消关注', icon: 'none' })
|
} else {
|
||||||
} else {
|
|
||||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 关注
|
// 关注
|
||||||
try {
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const success = await supabaseService.followShop(shopId, userId)
|
||||||
const success = await supabaseService.followShop(shopId, userId)
|
if (success) {
|
||||||
if (success) {
|
isFollowed.value = true
|
||||||
isFollowed.value = true
|
uni.showToast({ title: '关注成功', icon: 'success' })
|
||||||
uni.showToast({ title: '关注成功', icon: 'success' })
|
} else {
|
||||||
} else {
|
|
||||||
uni.showToast({ title: '关注失败', icon: 'none' })
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
uni.showToast({ title: '关注失败', icon: 'none' })
|
uni.showToast({ title: '关注失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,20 +440,50 @@ const contactService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addToCart = async (product: ProductType) => {
|
const addToCart = async (product: ProductType) => {
|
||||||
uni.showLoading({ title: '添加中...' })
|
uni.showLoading({ title: '检查商品...' })
|
||||||
|
|
||||||
const success = await supabaseService.addToCart(product.id, 1, '')
|
try {
|
||||||
|
// 使用店铺的 merchant_id
|
||||||
|
const merchantId = merchant.value.user_id ?? ''
|
||||||
|
|
||||||
uni.hideLoading()
|
// 检查商品是否有SKU
|
||||||
|
const skus = await supabaseService.getProductSkus(product.id)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
if (success) {
|
if (skus.length > 0) {
|
||||||
|
// 有规格,提示并跳转到商品详情页选择规格
|
||||||
|
uni.showToast({
|
||||||
|
title: '请选择规格',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/mall/consumer/product-detail?id=' + product.id
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
// 无规格,直接加入购物车
|
||||||
|
uni.showLoading({ title: '添加中...' })
|
||||||
|
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已添加到购物车',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '添加失败,请重试',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('添加到购物车异常', e)
|
||||||
|
uni.hideLoading()
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '已添加到购物车',
|
title: '操作失败',
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: '添加失败,请重试',
|
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -411,89 +494,6 @@ const goToProduct = (id: string) => {
|
|||||||
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
url: `/pages/mall/consumer/product-detail?productId=${id}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const claimCoupon = async (coupon: CouponTemplateType): Promise<void> => {
|
|
||||||
const userId = supabaseService.getCurrentUserId()
|
|
||||||
if (userId == null) {
|
|
||||||
uni.navigateTo({ url: '/pages/auth/login' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uni.showLoading({ title: '领取中' })
|
|
||||||
|
|
||||||
let success = false
|
|
||||||
const couponId = coupon.id
|
|
||||||
|
|
||||||
try {
|
|
||||||
success = await supabaseService.claimShopCoupon(couponId, userId)
|
|
||||||
} catch(e) {
|
|
||||||
try {
|
|
||||||
success = await supabaseService.claimCoupon(couponId, userId)
|
|
||||||
} catch(e2) {
|
|
||||||
console.warn('claimCoupon not found')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.hideLoading()
|
|
||||||
if (success) {
|
|
||||||
uni.showToast({ title: '领取成功', icon: 'success' })
|
|
||||||
} else {
|
|
||||||
uni.showToast({ title: '领取失败', icon: 'none' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRefresherRefresh = (): void => {
|
|
||||||
isRefresherTriggered.value = true
|
|
||||||
currentPage.value = 1
|
|
||||||
hasMore.value = true
|
|
||||||
isLoading.value = false
|
|
||||||
|
|
||||||
if (currentMerchantId.value != '') {
|
|
||||||
const id = currentMerchantId.value
|
|
||||||
loadShopData(id)
|
|
||||||
loadCoupons(id)
|
|
||||||
loadShopProducts(id)
|
|
||||||
setTimeout(() => {
|
|
||||||
isRefresherTriggered.value = false
|
|
||||||
}, 500)
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
isRefresherTriggered.value = false
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onScrollToLower = (): void => {
|
|
||||||
if (hasMore.value && isLoading.value == false && currentMerchantId.value != '') {
|
|
||||||
console.log('Scroll to lower, loading more...')
|
|
||||||
loadShopProducts(currentMerchantId.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
const options = pages[pages.length - 1].options as UTSJSONObject
|
|
||||||
const mId = options.getString('merchantId')
|
|
||||||
const pId = options.getString('id')
|
|
||||||
const paramId = (mId != null ? mId : pId) as string
|
|
||||||
|
|
||||||
if (paramId != null && paramId !== '') {
|
|
||||||
console.log('Page mounted with params:', paramId)
|
|
||||||
loadShopData(paramId)
|
|
||||||
loadShopProducts(paramId)
|
|
||||||
loadCoupons(paramId)
|
|
||||||
} else {
|
|
||||||
console.error('No ID passed to shop-detail')
|
|
||||||
uni.showToast({title: '参数错误', icon: 'error'})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
|
||||||
onRefresherRefresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
onReachBottom(() => {
|
|
||||||
onScrollToLower()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -704,83 +704,68 @@ onReachBottom(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-item {
|
.product-item {
|
||||||
width: 48%; /* Fallback for calc(50% - 5px) */
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 10px;
|
background: #fff;
|
||||||
box-sizing: border-box;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 48%;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 170px;
|
height: 170px;
|
||||||
background-color: #f5f5f5;
|
border-radius: 8px;
|
||||||
}
|
margin-bottom: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
.product-info {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 5px;
|
||||||
text-overflow: ellipsis;
|
line-height: 1.4;
|
||||||
lines: 2;
|
height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 40px;
|
text-overflow: ellipsis;
|
||||||
line-height: 20px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-row {
|
.product-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
padding: 0 8px 8px;
|
||||||
|
|
||||||
.price-left {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-btn {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background-color: #ff4444;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-price {
|
.product-price {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
color: #ff4444;
|
color: #ff5000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-sales {
|
.product-add-btn {
|
||||||
font-size: 12px;
|
width: 24px;
|
||||||
color: #999;
|
height: 24px;
|
||||||
|
background-color: #ff5000;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-icon {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PC/Tablet Responsive */
|
/* PC/Tablet Responsive */
|
||||||
/* Note: UTS/uni-app x media queries support depends on platform.
|
|
||||||
On Web/H5 this works standard. On App, width is fixed based on screen.
|
|
||||||
Using standard CSS media queries for H5/PC adaptation.
|
|
||||||
*/
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.product-item {
|
.product-item {
|
||||||
width: 32% !important; /* Tablet: 3 items */
|
width: 32% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
BIN
unpackage/cache/.app-android/class/ktClasss.ser
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user