20260227-1

This commit is contained in:
cyh666666
2026-02-27 16:51:56 +08:00
1526 changed files with 2457 additions and 38509 deletions

View 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
View 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
View 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!")

View File

@@ -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

View File

@@ -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,14 +560,33 @@ 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') ?? '' // 检查商品是否有SKU
const success = await supabaseService.addToCart(productId, 1, skuId) 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, skuId, merchantId)
uni.hideLoading()
if (success) { if (success) {
uni.showToast({ uni.showToast({
title: '已添加到购物车', title: '已添加到购物车',
@@ -581,8 +602,10 @@ const addToCart = async (product: any) => {
icon: 'none' icon: 'none'
}) })
} }
}
} catch (error) { } catch (error) {
console.error('添加商品到购物车异常:', error) console.error('添加商品到购物车异常:', error)
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '添加失败', title: '添加失败',
icon: 'none' icon: 'none'

View File

@@ -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,15 +450,36 @@ 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, '')
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(pid)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
uni.showToast({
title: '请选择规格',
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) { if (success) {
uni.showToast({ uni.showToast({
title: '已添加到购物车', title: '已添加到购物车',
@@ -485,11 +492,11 @@ async function addToCart(product: Product): Promise<void> {
icon: 'none' 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 {

View File

@@ -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

View File

@@ -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()` 等方法安全访问属性
================================================================================
================================================================================

View File

@@ -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,28 +24,43 @@
<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 {
const merchantId = product.merchant_id ?? product.shop_id ?? ''
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(product.id)
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() uni.hideLoading()
if (success) { if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' }) uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else { } else {
uni.showToast({ title: '添加失败', icon: 'none' }) uni.showToast({ title: '添加失败', icon: 'none' })
} }
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ title: '操作失败', icon: 'none' })
}
} }
const loadFavorites = async () => { const loadFavorites = async () => {
@@ -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>

View File

@@ -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">
<view class="product-price-row"> <text class="product-price">¥{{ item.price }}</text>
<text class="current-price">¥{{ item.price }}</text> <view class="product-add-btn" @click.stop="addToCart(item)">
</view> <text class="add-icon">+</text>
</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,6 +178,12 @@ const clearAll = () => {
content: '确定要清空所有浏览记录吗?', content: '确定要清空所有浏览记录吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.showLoading({ title: '清空中...' })
supabaseService.clearFootprints().then((success) => {
uni.hideLoading()
if (success) {
footprints.value = [] footprints.value = []
uni.removeStorageSync('footprints') uni.removeStorageSync('footprints')
@@ -185,6 +191,13 @@ const clearAll = () => {
title: '已清空', title: '已清空',
icon: 'success' icon: 'success'
}) })
} else {
uni.showToast({
title: '清空失败',
icon: 'none'
})
}
})
} }
} }
}) })
@@ -238,12 +251,23 @@ const deleteSelected = () => {
content: `确定要删除选中的${selectedItems.length}条记录吗?`, content: `确定要删除选中的${selectedItems.length}条记录吗?`,
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.showLoading({ title: '删除中' }) uni.showLoading({ title: '删除中...' })
// 收集要删除的商品ID
const productIds: string[] = []
for (let i = 0; i < selectedItems.length; i++) {
productIds.push(selectedItems[i].id)
}
// 调用服务层批量删除
supabaseService.deleteFootprints(productIds).then((success) => {
uni.hideLoading() uni.hideLoading()
if (success) {
// 从本地列表中移除
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true) footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
// 更新本地缓存
const dataToSave: FootprintSaveType[] = [] const dataToSave: FootprintSaveType[] = []
for (let i = 0; i < footprints.value.length; i++) { for (let i = 0; i < footprints.value.length; i++) {
const item = footprints.value[i] const item = footprints.value[i]
@@ -269,11 +293,66 @@ const deleteSelected = () => {
if (footprints.value.length === 0) { if (footprints.value.length === 0) {
isEditMode.value = false isEditMode.value = false
} }
} else {
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
})
} }
} }
}) })
} }
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) {

View File

@@ -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)}&timestamp=${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)}&timestamp=${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,34 +810,52 @@ 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, '')
// 检查商品是否有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) { if (success) {
uni.showToast({ uni.showToast({
title: '已添加到购物车', title: '已添加到购物车',
icon: 'success' icon: 'success'
}) })
} else { } else {
// 失败(如未登录),回退到本地存储或提示登录
// 这里简单提示失败
uni.showToast({ uni.showToast({
title: '添加失败,请先登录', title: '添加失败,请先登录',
icon: 'none' icon: 'none'
}) })
} }
}
} catch (e) { } catch (e) {
console.error('添加到购物车异常', e) console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ uni.showToast({
title: '操作异常', title: '操作异常',
icon: 'none' 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 */ .product-bottom {
display: flex;
white-space: normal;
}
.product-spec {
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 {

View File

@@ -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: '已添加到购物车',

View File

@@ -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 '购物奖励'
} else if (type == 'redeem') {
return '积分兑换'
} else if (type == 'admin') {
return '系统调整'
} else if (type == 'register') {
return '注册赠送'
} else {
return '积分变动' return '积分变动'
}
} }
const formatTime = (timeStr: string): string => { const formatTime = (timeStr: string): string => {

View File

@@ -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')

View File

@@ -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 !== '') {

View File

@@ -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" />
</view> <text class="guess-name" :lines="2">{{ item.name }}</text>
<view class="guess-info"> <view class="guess-bottom">
<text class="guess-name">{{ item.name }}</text> <text class="guess-price">¥{{ item.price }}</text>
<view class="guess-price-row"> <view class="guess-add-btn" @click.stop="addToCart(item)">
<text class="price-symbol">¥</text> <text class="guess-add-icon">+</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-tags-row" v-if="product.tag">
<text class="product-tag">{{ product.tag }}</text>
</view>
<text class="product-spec">{{ product.specification }}</text>
<view class="product-bottom"> <view class="product-bottom">
<view class="price-box"> <text class="product-price">¥{{ product.price }}</text>
<text class="price-symbol">¥</text> <view class="product-add-btn" @click.stop="addToCart(product)">
<text class="price-value">{{ product.price }}</text> <text class="add-icon">+</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,10 +600,33 @@ onMounted(() => {
}) })
const onInput = (e: any) => { const onInput = (e: any) => {
try {
let val = ''
// 处理 input 事件的不同事件对象格式
if (e != null) {
// UTSJSONObject 格式 (e.detail.value)
if (e instanceof UTSJSONObject) {
const eObj = e as UTSJSONObject const eObj = e as UTSJSONObject
const detailRaw = eObj.get('detail') const detailObj = eObj.get('detail')
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject()) if (detailObj != null && detailObj instanceof UTSJSONObject) {
const val = detail.getString('value') ?? '' 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 searchKeyword.value = val
if (val == '') { if (val == '') {
showResults.value = false showResults.value = false
@@ -614,6 +638,9 @@ const onInput = (e: any) => {
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.showLoading({ title: '检查商品...' })
try {
// 统一转换为 UTSJSONObject 访问属性
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' }) uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => { setTimeout(() => {
viewProductDetail(product) uni.navigateTo({
}, 800) 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;
} }

View File

@@ -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">
<view class="price-left">
<text class="product-price">¥{{ product.price }}</text> <text class="product-price">¥{{ product.price }}</text>
<text class="product-sales">已售 {{ product.sales }}</text> <view class="product-add-btn" @click.stop="addToCart(product)">
</view> <text class="add-icon">+</text>
<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 if (typeof imageUrls === 'string') {
const rawUrl = imageUrls as string
if (rawUrl.startsWith('[')) {
const parsed = JSON.parse(rawUrl)
if (Array.isArray(parsed)) {
const arr = parsed as string[]
if (images.length == 0) images.push(...arr)
} }
} else { } else {
const rawUrlStr = imageUrlsRaw as string if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
if (rawUrlStr.startsWith('[')) {
const parsed = JSON.parse(rawUrlStr)
if (Array.isArray(parsed) && images.length == 0) {
for (let i: number = 0; i < parsed.length; i++) {
images.push(parsed[i] as string)
}
}
} else {
if (images.indexOf(rawUrlStr) === -1) images.push(rawUrlStr)
} }
} }
} catch(e) { } catch(e) {
@@ -248,76 +246,35 @@ 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])
} }
} currentPage.value++
hasMore.value = list.length >= pageSize.value
// 判断是否还有更多
if (list.length < pageSize.value) {
hasMore.value = false
} else { } else {
hasMore.value = true
currentPage.value++ // 准备下一页
}
} else {
console.log('未加载到店铺商品 (本页为空)')
if (currentPage.value === 1) {
products.value = []
}
hasMore.value = false hasMore.value = false
} }
@@ -325,7 +282,110 @@ const loadShopProducts = async (id: string): Promise<void> => {
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,9 +398,9 @@ 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) {
@@ -349,12 +409,8 @@ const toggleFollow = async (): Promise<void> => {
} else { } else {
uni.showToast({ title: '操作失败', icon: 'none' }) uni.showToast({ title: '操作失败', icon: 'none' })
} }
} catch(e) {
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) {
@@ -363,9 +419,6 @@ const toggleFollow = async (): Promise<void> => {
} else { } else {
uni.showToast({ title: '关注失败', icon: 'none' }) uni.showToast({ title: '关注失败', icon: 'none' })
} }
} catch(e) {
uni.showToast({ title: '关注失败', icon: 'none' })
}
} }
uni.hideLoading() uni.hideLoading()
} }
@@ -387,10 +440,31 @@ const contactService = () => {
} }
const addToCart = async (product: ProductType) => { const addToCart = async (product: ProductType) => {
uni.showLoading({ title: '检查商品...' })
try {
// 使用店铺的 merchant_id
const merchantId = merchant.value.user_id ?? ''
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(product.id)
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: '添加中...' }) uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
const success = await supabaseService.addToCart(product.id, 1, '')
uni.hideLoading() uni.hideLoading()
if (success) { if (success) {
@@ -404,6 +478,15 @@ const addToCart = async (product: ProductType) => {
icon: 'none' icon: 'none'
}) })
} }
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
} }
const goToProduct = (id: string) => { const goToProduct = (id: string) => {
@@ -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.

Some files were not shown because too many files have changed in this diff Show More