20260227-1
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,7 @@ type RecommendProduct = {
|
||||
price: number
|
||||
image: string
|
||||
skuId: string
|
||||
merchant_id: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
@@ -326,9 +327,10 @@ const loadCartData = async () => {
|
||||
shopId: p.merchant_id ?? 'unknown',
|
||||
shopName: p.shop_name ?? '商城推荐',
|
||||
name: p.name,
|
||||
price: p.base_price ?? p.price ?? 0,
|
||||
image: p.main_image_url ?? '/static/images/default-product.png',
|
||||
skuId: ''
|
||||
price: p.base_price ?? p.market_price ?? 0,
|
||||
image: p.main_image_url ?? p.image_url ?? '/static/images/default-product.png',
|
||||
skuId: '',
|
||||
merchant_id: p.merchant_id ?? ''
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -558,31 +560,52 @@ const deleteSelectedItems = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = async (product: any) => {
|
||||
const addToCart = async (product: RecommendProduct) => {
|
||||
uni.showLoading({ title: '检查商品...' })
|
||||
try {
|
||||
// 调用SupabaseService添加商品到购物车
|
||||
// 显式访问属性,避免any类型导致的编译错误
|
||||
const target = product as UTSJSONObject
|
||||
const productId = target.getString('id') ?? ''
|
||||
const skuId = target.getString('skuId') ?? ''
|
||||
const success = await supabaseService.addToCart(productId, 1, skuId)
|
||||
if (success) {
|
||||
const productId = product.id
|
||||
const skuId = product.skuId
|
||||
const merchantId = product.merchant_id
|
||||
|
||||
// 检查商品是否有SKU
|
||||
const skus = await supabaseService.getProductSkus(productId)
|
||||
uni.hideLoading()
|
||||
|
||||
if (skus.length > 0) {
|
||||
// 有规格,提示并跳转到商品详情页选择规格
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载购物车数据
|
||||
loadCartData()
|
||||
} else {
|
||||
console.error('添加商品到购物车失败')
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
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) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载购物车数据
|
||||
loadCartData()
|
||||
} else {
|
||||
console.error('添加商品到购物车失败')
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加商品到购物车异常:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
|
||||
@@ -65,30 +65,16 @@
|
||||
class="product-card"
|
||||
@click="navigateToProduct(product)"
|
||||
>
|
||||
<view class="product-badge" v-if="product.is_hot">热销</view>
|
||||
<image
|
||||
class="product-image"
|
||||
:src="product.main_image_url"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
|
||||
<view class="price-section">
|
||||
<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>
|
||||
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<text class="product-price">¥{{ product.base_price ?? product.price ?? 0 }}</text>
|
||||
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -464,32 +450,53 @@ onShow(() => {
|
||||
|
||||
// 添加到购物车
|
||||
async function addToCart(product: Product): Promise<void> {
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
uni.showLoading({ title: '检查商品...' })
|
||||
try {
|
||||
const pid = (product.id ?? '').toString()
|
||||
const merchantId = product.merchant_id ?? ''
|
||||
if (pid === '') {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '商品无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const success = await supabaseService.addToCart(pid, 1, '')
|
||||
if (success) {
|
||||
|
||||
// 检查商品是否有SKU
|
||||
const skus = await supabaseService.getProductSkus(pid)
|
||||
uni.hideLoading()
|
||||
|
||||
if (skus.length > 0) {
|
||||
// 有规格,提示并跳转到商品详情页选择规格
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
cartCount.value++
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '添加失败,请先登录',
|
||||
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) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
cartCount.value++
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '添加失败,请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('添加到购物车异常', e)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,111 +801,70 @@ function onScan(): void {
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
/* cursor: pointer; removed for uniapp-x support */
|
||||
transition: all 0.3s ease;
|
||||
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;
|
||||
width: 48%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
/* object-fit: cover; REMOVED for uniapp-x support - default behavior is often acceptable or handle via image mode */
|
||||
background: white;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 16px;
|
||||
height: 170px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
/* display: block; REMOVED for uniapp-x support */
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
/* display: block; REMOVED for uniapp-x support */
|
||||
}
|
||||
|
||||
.price-section {
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
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;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.manufacturer {
|
||||
color: #666;
|
||||
.product-price {
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sales-count {
|
||||
color: #999;
|
||||
.product-add-btn {
|
||||
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 {
|
||||
|
||||
@@ -1309,6 +1309,12 @@ const submitOrder = async () => {
|
||||
const groups: any[] = []
|
||||
for (let i = 0; i < shopGroups.value.length; 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[] = []
|
||||
for (let j = 0; j < group.items.length; j++) {
|
||||
const item = group.items[j]
|
||||
@@ -1323,8 +1329,10 @@ const submitOrder = async () => {
|
||||
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({
|
||||
merchant_id: (group.merchant_id != null && group.merchant_id != '') ? group.merchant_id : group.shopId,
|
||||
merchant_id: finalMerchantId,
|
||||
shopId: group.shopId,
|
||||
shopName: group.shopName,
|
||||
items: items
|
||||
|
||||
@@ -1394,3 +1394,598 @@ getCurrentUserId 函数 - 将可选链替换为显式 null 检查和 UTSJSONObje
|
||||
================================================================================
|
||||
文档结束
|
||||
================================================================================
|
||||
|
||||
================================================================================
|
||||
二十四、2026-02-27 函数可选参数限制(重要)
|
||||
================================================================================
|
||||
|
||||
1. 可选参数不能跳过传递
|
||||
- UTS Android 不支持跳过可选参数传递
|
||||
- 如果函数有多个可选参数,必须按顺序传递所有参数
|
||||
- 错误示例:
|
||||
```typescript
|
||||
// 函数定义
|
||||
async addToCart(productId: string, quantity: number = 1, skuId?: string, merchantId?: string): Promise<boolean>
|
||||
|
||||
// 错误调用 - 跳过了 merchantId 参数
|
||||
await supabaseService.addToCart(productId, 1, '')
|
||||
// 编译错误:No value passed for parameter 'merchantId'
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
// 方案1:给可选参数添加默认值
|
||||
async addToCart(productId: string, quantity: number = 1, skuId: string = '', merchantId: string = ''): Promise<boolean>
|
||||
|
||||
// 方案2:调用时传递所有参数
|
||||
await supabaseService.addToCart(productId, 1, '', '')
|
||||
```
|
||||
|
||||
2. 可选参数定义规范
|
||||
- 推荐使用 `param: Type = defaultValue` 而非 `param?: Type`
|
||||
- `param?: Type` 在 Android 端调用时仍需传递参数
|
||||
- `param: Type = defaultValue` 可以在不传参时使用默认值
|
||||
- 示例:
|
||||
```typescript
|
||||
// 不推荐 - 调用时仍需传递参数
|
||||
function foo(a: string, b?: string, c?: string): void
|
||||
|
||||
// 推荐 - 可以跳过参数使用默认值
|
||||
function foo(a: string, b: string = '', c: string = ''): void
|
||||
```
|
||||
|
||||
3. 编译错误提示
|
||||
- 错误信息:"No value passed for parameter 'xxx'"
|
||||
- 原因:可选参数在 Android 端不能跳过
|
||||
- 解决:
|
||||
1. 修改函数签名,使用默认值 `param: Type = defaultValue`
|
||||
2. 调用时传递所有参数
|
||||
|
||||
4. 最佳实践
|
||||
- 对于有多个可选参数的函数,统一使用默认值语法
|
||||
- 调用时显式传递所有参数,避免依赖可选参数跳过
|
||||
- 在服务层函数定义中,优先使用 `= ''` 或 `= 0` 等默认值
|
||||
|
||||
================================================================================
|
||||
二十五、2026-02-27 模板中的非空断言限制(重要)
|
||||
================================================================================
|
||||
|
||||
1. 模板中不支持非空断言操作符 `!`
|
||||
- UTS Android 模板中不能使用 `variable!` 非空断言
|
||||
- 错误示例:
|
||||
```html
|
||||
<text v-if="product.original_price != null && product.original_price! > product.price">
|
||||
```
|
||||
- 正确示例:
|
||||
```html
|
||||
<text v-if="product.original_price != null && product.original_price > product.price">
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"参数类型不匹配:实际类型为 'Number?',预期类型为 'Number'"
|
||||
- 原因:模板中使用非空断言 `!` 不被支持
|
||||
- 解决:移除非空断言 `!`,直接使用变量进行比较
|
||||
|
||||
3. 最佳实践
|
||||
- 在模板中,先用 `!= null` 判断可空类型,然后直接使用变量
|
||||
- UTS 编译器会在 `!= null` 判断后自动识别变量为非空类型
|
||||
|
||||
================================================================================
|
||||
二十六、2026-02-27 未导入类型的处理(重要)
|
||||
================================================================================
|
||||
|
||||
1. 未导入的类型不能直接使用
|
||||
- 在页面中使用的类型必须先导入或使用 UTSJSONObject 替代
|
||||
- 错误示例:
|
||||
```typescript
|
||||
// Shop 类型未导入
|
||||
const s = shopRespData[i] as Shop
|
||||
const id = s.id // 找不到名称 "id"
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
// 使用 UTSJSONObject
|
||||
const s = shopRespData[i] as UTSJSONObject
|
||||
const id = s.getString('id') ?? ''
|
||||
const name = s.getString('shop_name') ?? ''
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"找不到名称 'XXX'"
|
||||
- 原因:类型未导入或类型定义不存在
|
||||
- 解决:
|
||||
1. 导入需要的类型:`import { Shop } from '@/utils/supabaseService.uts'`
|
||||
2. 使用 UTSJSONObject 替代:`as UTSJSONObject` 然后用 `getString()`、`getNumber()` 访问属性
|
||||
|
||||
3. 最佳实践
|
||||
- 对于简单的数据转换,推荐使用 UTSJSONObject
|
||||
- 避免在多个文件中重复定义相同的类型
|
||||
- 如果需要类型安全,从服务层导入类型定义
|
||||
|
||||
================================================================================
|
||||
二十七、2026-02-27 服务层数据字段完整性(重要)
|
||||
================================================================================
|
||||
|
||||
1. 服务层返回数据必须包含所有必要字段
|
||||
- 从数据库获取数据时,必须正确映射所有需要的字段
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const product: Product = {
|
||||
id: prodObj.getString('id') ?? '',
|
||||
name: prodObj.getString('name') ?? '',
|
||||
// 错误:merchant_id 硬编码为空字符串
|
||||
merchant_id: ''
|
||||
} as Product
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const product: Product = {
|
||||
id: prodObj.getString('id') ?? '',
|
||||
name: prodObj.getString('name') ?? '',
|
||||
// 正确:从数据库获取 merchant_id
|
||||
merchant_id: prodObj.getString('merchant_id') ?? ''
|
||||
} as Product
|
||||
```
|
||||
|
||||
2. 调用服务层方法时必须传递完整参数
|
||||
- 页面调用服务层方法时,需要传递所有必要参数
|
||||
- 错误示例:
|
||||
```typescript
|
||||
// 错误:merchant_id 传空字符串
|
||||
await supabaseService.addToCart(productId, 1, '', '')
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
// 正确:从商品对象获取 merchant_id
|
||||
const merchantId = product.merchant_id ?? ''
|
||||
await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||
```
|
||||
|
||||
3. 编译错误提示
|
||||
- 问题表现:数据添加到数据库失败,或添加的数据不完整
|
||||
- 原因:服务层或页面层缺少必要字段的传递
|
||||
- 解决:
|
||||
1. 检查服务层数据映射是否完整
|
||||
2. 检查页面调用时是否传递了所有必要参数
|
||||
|
||||
4. 最佳实践
|
||||
- 服务层方法返回的对象应包含数据库视图的所有字段
|
||||
- 页面调用服务层方法时,应从数据对象中获取并传递所有参数
|
||||
- 对于关联数据(如 merchant_id),确保在数据加载时一并获取
|
||||
|
||||
================================================================================
|
||||
二十八、2026-02-27 模板中的非运算符限制(重要)
|
||||
================================================================================
|
||||
|
||||
1. 模板中不支持 `!` 非运算符
|
||||
- UTS Android 模板中不能使用 `!variable` 非运算符
|
||||
- 错误示例:
|
||||
```html
|
||||
<view v-if="!brand.logo_url">
|
||||
```
|
||||
- 正确示例:
|
||||
```html
|
||||
<view v-if="brand.logo_url == null || brand.logo_url == ''">
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"找不到名称'not'"
|
||||
- 原因:模板中不支持非运算符 `!`
|
||||
- 解决:使用显式的比较表达式替代
|
||||
|
||||
3. 最佳实践
|
||||
- 使用 `== null` 或 `== ''` 检查空值
|
||||
- 使用 `!= null && != ''` 检查非空值
|
||||
|
||||
================================================================================
|
||||
二十九、2026-02-27 索引访问限制(重要)
|
||||
================================================================================
|
||||
|
||||
1. 不支持 `(obj as any)['key']` 索引访问方式
|
||||
- UTS Android 不支持对 any 类型使用索引访问
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const detail = (e as any)['detail']
|
||||
val = detail['value'] ?? ''
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
// 方案1:使用 UTSJSONObject
|
||||
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
|
||||
const detail = eObj.get('detail') as UTSJSONObject
|
||||
val = detail.getString('value') ?? ''
|
||||
|
||||
// 方案2:先判断类型再转换
|
||||
if (e instanceof UTSJSONObject) {
|
||||
const eObj = e as UTSJSONObject
|
||||
const detail = eObj.get('detail') as UTSJSONObject
|
||||
val = detail.getString('value') ?? ''
|
||||
}
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch"
|
||||
- 原因:any 类型不支持索引访问
|
||||
- 解决:转换为 UTSJSONObject 后使用 `.get()` 方法
|
||||
|
||||
3. 最佳实践
|
||||
- 统一使用 UTSJSONObject 处理动态对象
|
||||
- 使用 `.get()`、`.getString()`、`.getNumber()` 方法访问属性
|
||||
- 对于复杂对象,先用 `JSON.parse(JSON.stringify(obj))` 转换
|
||||
|
||||
================================================================================
|
||||
三十、2026-02-27 字符串不能直接作为布尔条件(重要)
|
||||
================================================================================
|
||||
|
||||
1. 字符串不能直接作为 if 条件
|
||||
- UTS Android 不支持将字符串直接作为布尔条件判断
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const paramId = '123'
|
||||
if (paramId) { // 错误:字符串不能直接作为布尔条件
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const paramId = '123'
|
||||
if (paramId != null && paramId != '') { // 正确:显式判断
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"Condition type mismatch: inferred type is 'String' but 'Boolean' was expected"
|
||||
- 原因:字符串类型不能直接作为布尔条件
|
||||
- 解决:使用显式的比较表达式
|
||||
|
||||
3. 最佳实践
|
||||
- 使用 `!= null && != ''` 检查字符串非空
|
||||
- 使用 `== null || == ''` 检查字符串为空
|
||||
|
||||
================================================================================
|
||||
三十一、2026-02-27 函数定义顺序(重要)
|
||||
================================================================================
|
||||
|
||||
1. 函数必须在调用前定义
|
||||
- UTS Android 要求函数在调用之前完成定义
|
||||
- 这与 JavaScript 的函数提升不同
|
||||
- 错误示例:
|
||||
```typescript
|
||||
onMounted(() => {
|
||||
loadData() // 错误:loadData 还未定义
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const loadData = async () => {
|
||||
// ...
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData() // 正确:loadData 已定义
|
||||
})
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"找不到名称'xxx'"
|
||||
- 原因:函数在调用点之后定义
|
||||
- 解决:将函数定义移到调用之前
|
||||
|
||||
3. 最佳实践
|
||||
- 将所有函数定义放在生命周期钩子(onMounted、onShow 等)之前
|
||||
- 按依赖关系排序函数定义顺序
|
||||
|
||||
================================================================================
|
||||
三十二、2026-02-27 联合类型属性访问(重要)
|
||||
================================================================================
|
||||
|
||||
1. 联合类型不能直接访问属性
|
||||
- 当参数类型为联合类型(如 `A | B`)时,不能直接访问属性
|
||||
- 错误示例:
|
||||
```typescript
|
||||
type A = { id: string, name: string }
|
||||
type B = { id: string, title: string }
|
||||
|
||||
const foo = (item: A | B) => {
|
||||
const id = item.id // 错误:联合类型不能直接访问属性
|
||||
}
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const foo = (item: A | B) => {
|
||||
// 方案1:转换为 UTSJSONObject
|
||||
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||
const id = obj.getString('id') ?? ''
|
||||
|
||||
// 方案2:使用类型守卫
|
||||
if ('name' in item) {
|
||||
const id = item.id // 此时类型已收窄为 A
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"找不到名称'xxx'"
|
||||
- 原因:联合类型的属性访问受限
|
||||
- 解决:转换为 UTSJSONObject 或使用类型守卫
|
||||
|
||||
3. 最佳实践
|
||||
- 对于联合类型参数,统一转换为 UTSJSONObject 处理
|
||||
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||||
|
||||
================================================================================
|
||||
三十三、2026-02-27 any 类型变量不能赋值为 null(重要)
|
||||
================================================================================
|
||||
|
||||
1. any 类型变量不能赋值为 null
|
||||
- UTS Android 中 `any` 类型不能赋值为 `null`
|
||||
- 错误示例:
|
||||
```typescript
|
||||
let res: any = null // 错误:Null cannot be a value of a non-null type 'Any'
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
let res: any = {} // 正确:使用空对象
|
||||
// 或者
|
||||
let res: any | null = null // 使用联合类型
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"Null cannot be a value of a non-null type 'Any'"
|
||||
- 原因:any 类型不允许 null 值
|
||||
- 解决:使用空对象 `{}` 或联合类型 `any | null`
|
||||
|
||||
================================================================================
|
||||
三十四、2026-02-27 对象字面量类型推断问题(重要)
|
||||
================================================================================
|
||||
|
||||
1. 对象字面量直接赋值给 ref 可能类型不匹配
|
||||
- 当对象字面量直接赋值给特定类型的 ref 时,可能报类型不匹配错误
|
||||
- 错误示例:
|
||||
```typescript
|
||||
merchant.value = {
|
||||
id: shop.id,
|
||||
user_id: shop.merchant_id,
|
||||
// ...
|
||||
} // 错误:Assignment type mismatch
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
// 方案1:显式声明类型
|
||||
const merchantData: MerchantType = {
|
||||
id: shop.id,
|
||||
user_id: shop.merchant_id,
|
||||
// ...
|
||||
}
|
||||
merchant.value = merchantData
|
||||
|
||||
// 方案2:使用 as 类型断言
|
||||
merchant.value = {
|
||||
id: shop.id,
|
||||
user_id: shop.merchant_id,
|
||||
// ...
|
||||
} as MerchantType
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"Assignment type mismatch: actual type is '<anonymous>', but 'XXX' was expected"
|
||||
- 原因:对象字面量被推断为匿名类型
|
||||
- 解决:显式声明类型或使用类型断言
|
||||
|
||||
================================================================================
|
||||
三十五、2026-02-27 any 类型不能直接访问属性(重要)
|
||||
================================================================================
|
||||
|
||||
1. any 类型参数不能直接访问属性
|
||||
- 在 map、forEach 等回调中,any 类型的参数不能直接访问属性
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const list = rawList.map((item): ProductType => {
|
||||
const id = item.id // 错误:找不到名称"id"
|
||||
const name = item.name // 错误:找不到名称"name"
|
||||
})
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const list = rawList.map((item: any): ProductType => {
|
||||
// 方案1:转换为 UTSJSONObject
|
||||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||
const id = itemObj.getString('id') ?? ''
|
||||
const name = itemObj.getString('name') ?? ''
|
||||
|
||||
// 方案2:显式标注参数类型并使用索引
|
||||
// 注意:这种方式在 UTS Android 中也可能有问题
|
||||
})
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"找不到名称'xxx'"
|
||||
- 原因:any 类型的属性访问受限
|
||||
- 解决:转换为 UTSJSONObject 后使用 `.getString()` 等方法
|
||||
|
||||
================================================================================
|
||||
三十六、2026-02-27 类型断言不会添加方法(重要)
|
||||
================================================================================
|
||||
|
||||
1. `as UTSJSONObject` 不会给对象添加方法
|
||||
- 使用 `as UTSJSONObject` 只是类型断言,不会让普通对象获得 `getString` 等方法
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const profileObj = profile as UTSJSONObject
|
||||
const id = profileObj.getString('user_id') // 运行时错误:getString is not a function
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
// 必须使用 JSON.parse(JSON.stringify()) 进行真正的转换
|
||||
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
|
||||
const id = profileObj.getString('user_id') ?? ''
|
||||
```
|
||||
|
||||
2. 运行时错误提示
|
||||
- 错误信息:"XXX is not a function"
|
||||
- 原因:类型断言只是编译时行为,不会改变运行时对象的方法
|
||||
- 解决:使用 `JSON.parse(JSON.stringify())` 进行真正的对象转换
|
||||
|
||||
3. 最佳实践
|
||||
- 对于从 API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||||
- 使用 `instanceof UTSJSONObject` 检查对象类型
|
||||
- 不要依赖 `as` 类型断言来添加方法
|
||||
|
||||
================================================================================
|
||||
三十七、2026-02-27 类型必须包含所有必填字段(重要)
|
||||
================================================================================
|
||||
|
||||
1. 创建类型实例时必须包含所有必填字段
|
||||
- UTS 类型定义中的非可选字段(不带 `?`)都是必填的
|
||||
- 错误示例:
|
||||
```typescript
|
||||
export type ProductType = {
|
||||
id: string
|
||||
merchant_id: string // 必填
|
||||
category_id: string // 必填
|
||||
name: string
|
||||
// ...
|
||||
}
|
||||
|
||||
// 错误:缺少 merchant_id、category_id 等必填字段
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.price
|
||||
} as ProductType // 运行时错误:missing required property
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
return {
|
||||
id: itemObj.getString('id') ?? '',
|
||||
merchant_id: itemObj.getString('merchant_id') ?? '',
|
||||
category_id: itemObj.getString('category_id') ?? '',
|
||||
name: itemObj.getString('name') ?? '未知商品',
|
||||
description: itemObj.getString('description') ?? '',
|
||||
images: images,
|
||||
price: itemObj.getNumber('base_price') ?? 0,
|
||||
original_price: itemObj.getNumber('market_price') ?? 0,
|
||||
stock: itemObj.getNumber('total_stock') ?? 0,
|
||||
sales: itemObj.getNumber('sale_count') ?? 0,
|
||||
status: 1,
|
||||
created_at: itemObj.getString('created_at') ?? ''
|
||||
} as ProductType
|
||||
```
|
||||
|
||||
2. 运行时错误提示
|
||||
- 错误信息:"Failed to construct type, missing required property: xxx"
|
||||
- 原因:类型定义中有必填字段未提供
|
||||
- 解决:
|
||||
1. 检查类型定义,确认所有必填字段
|
||||
2. 为所有必填字段提供值,即使是空字符串或默认值
|
||||
|
||||
3. 最佳实践
|
||||
- 查看类型定义,确认哪些字段是必填的(不带 `?`)
|
||||
- 使用 `??` 运算符提供默认值
|
||||
- 对于可选字段,可以不提供或使用 `null`
|
||||
|
||||
================================================================================
|
||||
三十八、2026-02-27 回调函数不能是 async(重要)
|
||||
================================================================================
|
||||
|
||||
1. API 回调函数不能使用 async 修饰
|
||||
- uni API 的回调函数(如 showModal 的 success)不支持 async 函数
|
||||
- 错误示例:
|
||||
```typescript
|
||||
uni.showModal({
|
||||
title: '确认',
|
||||
content: '确定要删除吗?',
|
||||
success: async (res) => { // 错误:回调函数不能是 async
|
||||
if (res.confirm) {
|
||||
const result = await someAsyncFunction()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
uni.showModal({
|
||||
title: '确认',
|
||||
content: '确定要删除吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 使用 Promise.then() 代替 await
|
||||
someAsyncFunction().then((result) => {
|
||||
// 处理结果
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
2. 编译错误提示
|
||||
- 错误信息:"参数类型不匹配:实际类型为 'Function1<..., UTSPromise<Unit>>',预期类型为 'Function1<..., Unit>?'"
|
||||
- 原因:回调函数返回 Promise 而非 void
|
||||
- 解决:使用 `.then()` 代替 `await`
|
||||
|
||||
3. 最佳实践
|
||||
- 在回调函数中使用 `.then()` 处理异步操作
|
||||
- 将异步逻辑封装为单独的函数,在回调中调用
|
||||
|
||||
================================================================================
|
||||
三十九、2026-02-27 类型转换前必须检查类型(重要)
|
||||
================================================================================
|
||||
|
||||
1. 使用 `as` 类型转换前必须检查实际类型
|
||||
- 直接使用 `as string` 转换可能导致运行时类型转换异常
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const idVal = item['id']
|
||||
const id = idVal as string // 错误:如果 idVal 是其他类型会崩溃
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const idVal = item['id']
|
||||
const id = (idVal != null && typeof idVal == 'string') ? (idVal as string) : ''
|
||||
```
|
||||
|
||||
2. 运行时错误提示
|
||||
- 错误信息:"null cannot be cast to non-null type kotlin.String"
|
||||
- 错误信息:"java.lang.Boolean cannot be cast to java.lang.String"
|
||||
- 原因:直接类型转换时,实际类型与目标类型不匹配
|
||||
- 解决:使用 `typeof` 检查类型后再转换
|
||||
|
||||
3. 最佳实践
|
||||
- 使用 `typeof` 检查类型
|
||||
- 使用 `!= null` 检查空值
|
||||
- 提供默认值防止空指针异常
|
||||
|
||||
================================================================================
|
||||
四十、2026-02-27 UTSJSONObject 必须正确转换(重要)
|
||||
================================================================================
|
||||
|
||||
1. `as UTSJSONObject` 不会添加方法
|
||||
- 从数据库返回的数据需要正确转换为 UTSJSONObject
|
||||
- 错误示例:
|
||||
```typescript
|
||||
const item = rawList[i]
|
||||
const brandObj = item as UTSJSONObject // 错误:brandObj.getString 不存在
|
||||
```
|
||||
- 正确示例:
|
||||
```typescript
|
||||
const item = rawList[i]
|
||||
const brandObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||
const id = brandObj.getString('id') ?? ''
|
||||
```
|
||||
|
||||
2. 运行时错误提示
|
||||
- 错误信息:"getString is not a function"
|
||||
- 原因:对象没有正确转换为 UTSJSONObject
|
||||
- 解决:使用 `JSON.parse(JSON.stringify())` 进行转换
|
||||
|
||||
3. 最佳实践
|
||||
- 对于从数据库/API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||||
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||||
|
||||
================================================================================
|
||||
|
||||
================================================================================
|
||||
|
||||
@@ -8,20 +8,12 @@
|
||||
</view>
|
||||
|
||||
<view 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" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<image :src="product.main_image_url" class="product-image" mode="aspectFill" />
|
||||
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<view class="product-footer">
|
||||
<text class="product-sales">已售 {{ product.sales }}</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 class="product-add-btn" @click.stop="addToCart(product)">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -32,27 +24,42 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
import type { Product } from '@/utils/supabaseService.uts'
|
||||
|
||||
type Product = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
image: string
|
||||
sales: number
|
||||
shopId?: string
|
||||
shopName?: string
|
||||
}
|
||||
|
||||
const favorites = ref<Product[]>([])
|
||||
const favorites = ref<Array<Product>>([])
|
||||
|
||||
const addToCart = async (product: Product) => {
|
||||
uni.showLoading({ title: '添加中' })
|
||||
const success = await supabaseService.addToCart(product.id, 1, '')
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||
uni.showLoading({ title: '检查商品...' })
|
||||
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()
|
||||
if (success) {
|
||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('添加到购物车异常', e)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +83,7 @@ const loadFavorites = async () => {
|
||||
let name = '未知商品'
|
||||
let price = 0
|
||||
let sales = 0
|
||||
let merchantId = ''
|
||||
|
||||
if (prod != null) {
|
||||
let prodObj: UTSJSONObject
|
||||
@@ -90,6 +98,7 @@ const loadFavorites = async () => {
|
||||
price = prodObj.getNumber('base_price') ?? 0
|
||||
image = prodObj.getString('main_image_url') ?? image
|
||||
sales = prodObj.getNumber('sale_count') ?? 0
|
||||
merchantId = prodObj.getString('merchant_id') ?? ''
|
||||
|
||||
if (image === '/static/default-product.png') {
|
||||
const imgUrls = prodObj.getString('image_urls')
|
||||
@@ -112,10 +121,10 @@ const loadFavorites = async () => {
|
||||
id: id,
|
||||
name: name,
|
||||
price: price,
|
||||
image: image,
|
||||
sales: sales,
|
||||
shopId: '',
|
||||
shopName: ''
|
||||
category_id: '',
|
||||
merchant_id: merchantId,
|
||||
main_image_url: image,
|
||||
sale_count: sales
|
||||
} as Product
|
||||
})
|
||||
}
|
||||
@@ -213,107 +222,79 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.product-item {
|
||||
width: 48%; /* Default Mobile: 2 items per row */
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box; /* Important for grid */
|
||||
}
|
||||
|
||||
/* PC/Tablet Responsive */
|
||||
@media (min-width: 768px) {
|
||||
.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;
|
||||
}
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
text-overflow: ellipsis;
|
||||
lines: 2;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.product-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
.product-add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #ff5000;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 8px; /* Replacement for gap */
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
background-color: #ff5000;
|
||||
.add-icon {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
/* PC/Tablet Responsive */
|
||||
@media (min-width: 768px) {
|
||||
.product-item {
|
||||
width: 31% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
font-size: 14px;
|
||||
@media (min-width: 1024px) {
|
||||
.product-item {
|
||||
width: 15% !important;
|
||||
}
|
||||
|
||||
.product-grid, .header {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,12 +33,11 @@
|
||||
</view>
|
||||
<view class="item-content" @click="viewProduct(item)">
|
||||
<image class="product-image" :src="item.image" mode="aspectFill" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ item.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<view class="product-price-row">
|
||||
<text class="current-price">¥{{ item.price }}</text>
|
||||
</view>
|
||||
<text class="product-name" :lines="2">{{ item.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<text class="product-price">¥{{ item.price }}</text>
|
||||
<view class="product-add-btn" @click.stop="addToCart(item)">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -83,6 +82,7 @@ type FootprintType = {
|
||||
shopName: string
|
||||
viewTime: number
|
||||
selected: boolean
|
||||
merchant_id: string
|
||||
}
|
||||
|
||||
type FootprintGroup = {
|
||||
@@ -178,12 +178,25 @@ const clearAll = () => {
|
||||
content: '确定要清空所有浏览记录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
footprints.value = []
|
||||
uni.removeStorageSync('footprints')
|
||||
uni.showLoading({ title: '清空中...' })
|
||||
|
||||
uni.showToast({
|
||||
title: '已清空',
|
||||
icon: 'success'
|
||||
supabaseService.clearFootprints().then((success) => {
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
footprints.value = []
|
||||
uni.removeStorageSync('footprints')
|
||||
|
||||
uni.showToast({
|
||||
title: '已清空',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '清空失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -238,42 +251,108 @@ const deleteSelected = () => {
|
||||
content: `确定要删除选中的${selectedItems.length}条记录吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '删除中' })
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
|
||||
|
||||
const dataToSave: FootprintSaveType[] = []
|
||||
for (let i = 0; i < footprints.value.length; i++) {
|
||||
const item = footprints.value[i]
|
||||
dataToSave.push({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
original_price: item.original_price,
|
||||
image: item.image,
|
||||
sales: item.sales,
|
||||
shopId: item.shopId,
|
||||
shopName: item.shopName,
|
||||
viewTime: item.viewTime
|
||||
} as FootprintSaveType)
|
||||
// 收集要删除的商品ID
|
||||
const productIds: string[] = []
|
||||
for (let i = 0; i < selectedItems.length; i++) {
|
||||
productIds.push(selectedItems[i].id)
|
||||
}
|
||||
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
// 调用服务层批量删除
|
||||
supabaseService.deleteFootprints(productIds).then((success) => {
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
// 从本地列表中移除
|
||||
footprints.value = footprints.value.filter((item): Boolean => item.selected !== true)
|
||||
|
||||
// 更新本地缓存
|
||||
const dataToSave: FootprintSaveType[] = []
|
||||
for (let i = 0; i < footprints.value.length; i++) {
|
||||
const item = footprints.value[i]
|
||||
dataToSave.push({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
original_price: item.original_price,
|
||||
image: item.image,
|
||||
sales: item.sales,
|
||||
shopId: item.shopId,
|
||||
shopName: item.shopName,
|
||||
viewTime: item.viewTime
|
||||
} as FootprintSaveType)
|
||||
}
|
||||
uni.setStorageSync('footprints', JSON.stringify(dataToSave))
|
||||
|
||||
uni.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
if (footprints.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (footprints.value.length === 0) {
|
||||
isEditMode.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = async (item: FootprintType) => {
|
||||
uni.showLoading({ title: '检查商品...' })
|
||||
try {
|
||||
const productId = item.id
|
||||
const merchantId = item.merchant_id ?? item.shopId ?? ''
|
||||
|
||||
// 检查商品是否有SKU
|
||||
const skus = await supabaseService.getProductSkus(productId)
|
||||
uni.hideLoading()
|
||||
|
||||
if (skus.length > 0) {
|
||||
// 有规格,提示并跳转到商品详情页选择规格
|
||||
uni.showToast({
|
||||
title: '请选择规格',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
// 无规格,直接加入购物车
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('添加到购物车异常', e)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const viewProduct = (item: FootprintType) => {
|
||||
if (isEditMode.value) return
|
||||
|
||||
@@ -309,7 +388,8 @@ const parseFootprintItem = (item: any): FootprintType => {
|
||||
shopId: itemObj.getString('shopId') ?? '',
|
||||
shopName: itemObj.getString('shopName') ?? '',
|
||||
viewTime: itemObj.getNumber('viewTime') ?? 0,
|
||||
selected: false
|
||||
selected: false,
|
||||
merchant_id: itemObj.getString('merchant_id') ?? ''
|
||||
} as FootprintType
|
||||
}
|
||||
|
||||
@@ -493,10 +573,11 @@ onMounted(() => {
|
||||
.footprint-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: none;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48%;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -535,7 +616,6 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -543,48 +623,50 @@ onMounted(() => {
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
border-radius: 5px;
|
||||
margin-right: 0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0 4px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 6px;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
lines: 2;
|
||||
height: 40px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.product-price-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 16px;
|
||||
color: #ff4757;
|
||||
.product-price {
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-add-btn {
|
||||
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;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -128,10 +128,10 @@
|
||||
@click="switchBrand(brand)"
|
||||
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 v-else class="card-icon">
|
||||
<text class="card-icon-text">🏢</text>
|
||||
<view class="card-icon" v-if="brand.logo_url == null || brand.logo_url == ''">
|
||||
<text class="card-icon-text">{{ getBrandIcon(brand.name) }}</text>
|
||||
</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>
|
||||
</view>
|
||||
</view>
|
||||
@@ -200,38 +200,16 @@
|
||||
class="product-card"
|
||||
@click="navigateToProduct(product)"
|
||||
>
|
||||
<view class="product-badge" v-if="product.is_hot">热销</view>
|
||||
<image
|
||||
class="product-image"
|
||||
:src="product.main_image_url"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="product-info">
|
||||
<view class="product-name">{{ product.name }}</view>
|
||||
<!-- spec is omitted if not available -->
|
||||
|
||||
<view class="price-section">
|
||||
<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>
|
||||
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -333,7 +311,7 @@ const subCategories = ref<Category[]>([])
|
||||
const selectedParentCategory = ref<Category | null>(null)
|
||||
const showSubCategories = ref(false)
|
||||
|
||||
// 排序标签类型
|
||||
|
||||
type SortTab = {
|
||||
id: string
|
||||
name: string
|
||||
@@ -371,7 +349,7 @@ const healthNews = [
|
||||
}
|
||||
]
|
||||
|
||||
// 获取分类数据
|
||||
// 获取一级分类数据
|
||||
const loadCategories = async (): Promise<void> => {
|
||||
try {
|
||||
const categoriesData = await supabaseService.getParentCategories()
|
||||
@@ -397,6 +375,35 @@ const loadSubCategories = async (parentId: string): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
// 点击一级分类
|
||||
const onParentCategoryClick = async (category: Category): Promise<void> => {
|
||||
// 如果已经选中,则切换显示/隐藏二级分类
|
||||
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
|
||||
showSubCategories.value = !showSubCategories.value
|
||||
return
|
||||
}
|
||||
|
||||
// 选中新的分类
|
||||
selectedParentCategory.value = category
|
||||
showSubCategories.value = true
|
||||
|
||||
// 加载二级分类
|
||||
await loadSubCategories(category.id)
|
||||
}
|
||||
|
||||
// 点击二级分类
|
||||
const onSubCategoryClick = (category: Category): void => {
|
||||
// 跳转到分类页面
|
||||
uni.setStorageSync('selectedCategory', category.id)
|
||||
const timestamp = Date.now()
|
||||
const randomParam = Math.random().toString(36).substring(2, 8)
|
||||
const url = `/pages/mall/consumer/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}×tamp=${timestamp}&random=${randomParam}`
|
||||
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/category'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取品牌数据
|
||||
const loadBrands = async (): Promise<void> => {
|
||||
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
|
||||
|
||||
@@ -648,35 +680,6 @@ const resetNavbar = () => {
|
||||
lastScrollTop.value = 0
|
||||
}
|
||||
|
||||
// 点击一级分类
|
||||
const onParentCategoryClick = async (category: Category): Promise<void> => {
|
||||
// 如果已经选中,则切换显示/隐藏二级分类
|
||||
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
|
||||
showSubCategories.value = !showSubCategories.value
|
||||
return
|
||||
}
|
||||
|
||||
// 选中新的分类
|
||||
selectedParentCategory.value = category
|
||||
showSubCategories.value = true
|
||||
|
||||
// 加载二级分类
|
||||
await loadSubCategories(category.id)
|
||||
}
|
||||
|
||||
// 点击二级分类
|
||||
const onSubCategoryClick = (category: Category): void => {
|
||||
// 跳转到分类页面
|
||||
uni.setStorageSync('selectedCategory', category.id)
|
||||
const timestamp = Date.now()
|
||||
const randomParam = Math.random().toString(36).substring(2, 8)
|
||||
const url = `/pages/mall/consumer/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}×tamp=${timestamp}&random=${randomParam}`
|
||||
|
||||
uni.switchTab({
|
||||
url: '/pages/mall/consumer/category'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换分类 - 跳转到分类页面并传递分类ID
|
||||
const switchCategory = (category: any) => {
|
||||
console.log('=== switchCategory函数开始执行 ===')
|
||||
@@ -807,35 +810,53 @@ const loadMore = async () => {
|
||||
|
||||
// 添加到购物车
|
||||
const addToCart = async (product: any) => {
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
uni.showLoading({ title: '检查商品...' })
|
||||
try {
|
||||
// 将 product 转换为 UTSJSONObject 以访问属性
|
||||
const prodObj = (product instanceof UTSJSONObject) ? (product as UTSJSONObject) : (JSON.parse(JSON.stringify(product)) as UTSJSONObject)
|
||||
const productId = prodObj.getString('id') ?? ''
|
||||
// 尝试调用 Supabase 服务添加
|
||||
const success = await supabaseService.addToCart(productId, 1, '')
|
||||
if (success) {
|
||||
const merchantId = prodObj.getString('merchant_id') ?? ''
|
||||
|
||||
// 检查商品是否有SKU
|
||||
const skus = await supabaseService.getProductSkus(productId)
|
||||
uni.hideLoading()
|
||||
|
||||
if (skus.length > 0) {
|
||||
// 有规格,提示并跳转到商品详情页选择规格
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
// 失败(如未登录),回退到本地存储或提示登录
|
||||
// 这里简单提示失败
|
||||
uni.showToast({
|
||||
title: '添加失败,请先登录',
|
||||
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.showToast({
|
||||
title: '操作异常',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '操作异常',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 扫码功能
|
||||
@@ -1189,14 +1210,14 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
}
|
||||
|
||||
.category-card {
|
||||
width: 47%; /* 50 - 3 */
|
||||
margin: 0 1.5% 16px 1.5%;
|
||||
width: 18%; /* 一行5个 */
|
||||
margin: 0 1% 12px 1%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
/* cursor: pointer; removed for uniapp-x support */
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
@@ -1282,27 +1303,31 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: var(--card-color, #4CAF50);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-icon-text {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
lines: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
@@ -1311,6 +1336,78 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
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 {
|
||||
background: white;
|
||||
@@ -1513,125 +1610,62 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); /* 增强悬停阴影 */
|
||||
}
|
||||
|
||||
.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;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 180px; /* 默认稍微高一点 */
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1; /* 撑开剩余空间 */
|
||||
height: 170px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
/* display: flex; removed for uniapp-x text support */
|
||||
/* overflow: hidden; */
|
||||
/* text-overflow: ellipsis; */
|
||||
/* display: -webkit-box; */
|
||||
/* -webkit-line-clamp: 2; */
|
||||
/* -webkit-box-orient: vertical; */
|
||||
/* Simplified for compatibility */
|
||||
display: flex;
|
||||
white-space: normal;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.manufacturer {
|
||||
color: #666;
|
||||
.product-price {
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sales-count {
|
||||
color: #999;
|
||||
.product-add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #ff5000;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-action {
|
||||
margin-top: 12px;
|
||||
.add-icon {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
|
||||
@@ -674,7 +674,7 @@ const addToCart = async (product: any) => {
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
try {
|
||||
// 尝试调用 Supabase 服务添加
|
||||
const success = await supabaseService.addToCart(product.id, 1, '')
|
||||
const success = await supabaseService.addToCart(product.id, 1, '', '')
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
|
||||
@@ -55,11 +55,7 @@ const totalPoints = ref<number>(0)
|
||||
const records = ref<PointRecord[]>([])
|
||||
const loading = ref<boolean>(true)
|
||||
|
||||
const loadPoints = async () => {
|
||||
// 调用 service 获取积分 (需要supabaseService支持)
|
||||
// 暂时如果service没更新,先用mock
|
||||
// const res = await supabaseService.getUserPoints()
|
||||
// if (res != null) totalPoints.value = res
|
||||
const loadPoints = async (): Promise<void> => {
|
||||
try {
|
||||
const points = await supabaseService.getUserPoints()
|
||||
totalPoints.value = points
|
||||
@@ -68,26 +64,19 @@ const loadPoints = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadRecords = async () => {
|
||||
const loadRecords = async (): Promise<void> => {
|
||||
try {
|
||||
const list = await supabaseService.getPointRecords()
|
||||
const typedList: PointRecord[] = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i] as PointRecord
|
||||
typedList.push(item)
|
||||
}
|
||||
records.value = typedList
|
||||
records.value = list as PointRecord[]
|
||||
} catch (e) {
|
||||
console.error('获取积分记录失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
const loadData = async (): Promise<void> => {
|
||||
loading.value = true
|
||||
await Promise.all<void>([
|
||||
loadPoints(),
|
||||
loadRecords()
|
||||
])
|
||||
await loadPoints()
|
||||
await loadRecords()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -103,12 +92,20 @@ const handleExchange = () => {
|
||||
}
|
||||
|
||||
const getTypeText = (type: string): string => {
|
||||
if (type === 'signin') return '每日签到'
|
||||
if (type === 'shopping') return '购物奖励'
|
||||
if (type === 'redeem') return '积分兑换'
|
||||
if (type === 'admin') return '系统调整'
|
||||
if (type === 'register') return '注册赠送'
|
||||
return '积分变动'
|
||||
// 不支持 Record<string, string>,使用 if-else
|
||||
if (type == 'signin') {
|
||||
return '每日签到'
|
||||
} else if (type == 'shopping') {
|
||||
return '购物奖励'
|
||||
} else if (type == 'redeem') {
|
||||
return '积分兑换'
|
||||
} else if (type == 'admin') {
|
||||
return '系统调整'
|
||||
} else if (type == 'register') {
|
||||
return '注册赠送'
|
||||
} else {
|
||||
return '积分变动'
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timeStr: string): string => {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
type="number"
|
||||
:value="quantity.toString()"
|
||||
:min="1"
|
||||
:max="getMaxQuantity()"
|
||||
:max="getMaxQuantity()"
|
||||
@input="validateQuantity" />
|
||||
<view class="quantity-btn plus" @click="increaseQuantity">
|
||||
<text class="quantity-btn-text">+</text>
|
||||
@@ -265,7 +265,7 @@ export default {
|
||||
showSpec: false,
|
||||
selectedSkuId: '',
|
||||
selectedSpec: '',
|
||||
quantity: 1,
|
||||
quantity: 1 as number,
|
||||
isFavorite: 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({
|
||||
id: this.product.id,
|
||||
name: this.product.name,
|
||||
price: this.product.price,
|
||||
original_price: this.product.original_price, // 添加原价
|
||||
image: this.product.images[0],
|
||||
original_price: this.product.original_price,
|
||||
image: productImage,
|
||||
sales: this.product.sales,
|
||||
shopId: this.merchant.id,
|
||||
shopName: this.merchant.shop_name,
|
||||
@@ -386,6 +392,7 @@ export default {
|
||||
|
||||
if (dbProduct != null) {
|
||||
// Map DB product to local product
|
||||
const dbObj = dbProduct as UTSJSONObject
|
||||
this.product = {
|
||||
id: dbProduct['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
|
||||
if (attrs['specification'] != null) this.product.specification = attrs['specification'] as string
|
||||
if (attrs['usage'] != null) this.product.usage = attrs['usage'] as string
|
||||
// ... augment as needed
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
@@ -686,7 +692,7 @@ export default {
|
||||
// 简化处理,直接返回 JSON 字符串
|
||||
return JSON.stringify(specs)
|
||||
}
|
||||
return sku.sku_code
|
||||
return sku.sku_code ?? ''
|
||||
},
|
||||
|
||||
async addToCart() {
|
||||
@@ -704,7 +710,8 @@ export default {
|
||||
const success = await supabaseService.addToCart(
|
||||
this.product.id,
|
||||
this.quantity,
|
||||
this.selectedSkuId
|
||||
this.selectedSkuId,
|
||||
this.product.merchant_id
|
||||
)
|
||||
uni.hideLoading()
|
||||
|
||||
@@ -740,7 +747,10 @@ export default {
|
||||
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
|
||||
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')
|
||||
|
||||
@@ -491,13 +491,14 @@ export default {
|
||||
uAvatar = profile.getString('avatar_url') ?? ''
|
||||
uGender = profile.getNumber('gender') ?? 0
|
||||
} else {
|
||||
const profileObj = profile as UTSJSONObject
|
||||
uId = (profileObj.getString('user_id') ?? '') as string
|
||||
uPhone = (profileObj.getString('phone') ?? '') as string
|
||||
uEmail = (profileObj.getString('email') ?? '') as string
|
||||
uNickname = (profileObj.getString('nickname') ?? '') as string
|
||||
uAvatar = (profileObj.getString('avatar_url') ?? '') as string
|
||||
uGender = (profileObj.getNumber('gender') ?? 0) as number
|
||||
// 必须使用 JSON.parse(JSON.stringify()) 转换为 UTSJSONObject
|
||||
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
|
||||
uId = profileObj.getString('user_id') ?? ''
|
||||
uPhone = profileObj.getString('phone') ?? ''
|
||||
uEmail = profileObj.getString('email') ?? ''
|
||||
uNickname = profileObj.getString('nickname') ?? ''
|
||||
uAvatar = profileObj.getString('avatar_url') ?? ''
|
||||
uGender = profileObj.getNumber('gender') ?? 0
|
||||
}
|
||||
|
||||
if (uNickname === '' && uPhone !== '') {
|
||||
|
||||
@@ -119,15 +119,12 @@
|
||||
class="guess-item"
|
||||
@click="viewProductDetail(item)"
|
||||
>
|
||||
<view class="guess-img-box">
|
||||
<image class="guess-img" :src="item.image" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="guess-info">
|
||||
<text class="guess-name">{{ item.name }}</text>
|
||||
<view class="guess-price-row">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-num">{{ item.price }}</text>
|
||||
<text class="sales-text">已售{{ item.sales }}</text>
|
||||
<image class="guess-img" :src="item.image" mode="aspectFill" />
|
||||
<text class="guess-name" :lines="2">{{ item.name }}</text>
|
||||
<view class="guess-bottom">
|
||||
<text class="guess-price">¥{{ item.price }}</text>
|
||||
<view class="guess-add-btn" @click.stop="addToCart(item)">
|
||||
<text class="guess-add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -206,21 +203,11 @@
|
||||
@click="viewProductDetail(product)"
|
||||
>
|
||||
<image class="product-image" :src="product.image" mode="aspectFill" />
|
||||
<view class="product-info">
|
||||
<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="price-box">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ product.price }}</text>
|
||||
</view>
|
||||
<view class="add-cart-btn" @click.stop="addToCart(product)">
|
||||
<text class="cart-icon">+</text>
|
||||
</view>
|
||||
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -278,6 +265,7 @@ type GuessItemType = {
|
||||
price: number
|
||||
image: string
|
||||
sales: number
|
||||
merchant_id: string
|
||||
}
|
||||
|
||||
type SearchResultType = {
|
||||
@@ -288,6 +276,7 @@ type SearchResultType = {
|
||||
specification: string
|
||||
tag: string
|
||||
sales: number
|
||||
merchant_id: string
|
||||
}
|
||||
|
||||
type ShopResultType = {
|
||||
@@ -377,14 +366,23 @@ const loadData = async (): Promise<void> => {
|
||||
|
||||
try {
|
||||
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 limit1 = hotProducts.length < 10 ? hotProducts.length : 10
|
||||
for (let i: number = 0; i < limit1; i++) {
|
||||
const p = hotProducts[i] as UTSJSONObject
|
||||
const p = hotProducts[i]
|
||||
const item: HotSearchItemType = {
|
||||
keyword: p.getString('name') ?? '',
|
||||
keyword: p.name ?? '',
|
||||
hot: true
|
||||
}
|
||||
hotList.push(item)
|
||||
@@ -393,14 +391,15 @@ const loadData = async (): Promise<void> => {
|
||||
|
||||
const allItems: Array<GuessItemType> = []
|
||||
for (let i: number = 0; i < hotProducts.length; i++) {
|
||||
const p = hotProducts[i] as UTSJSONObject
|
||||
const saleCount = p.getNumber('sale_count')
|
||||
const p = hotProducts[i]
|
||||
const saleCount = p.sale_count
|
||||
const item: GuessItemType = {
|
||||
id: p.getString('id') ?? '',
|
||||
name: p.getString('name') ?? '',
|
||||
price: p.getNumber('base_price') ?? 0,
|
||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
||||
sales: saleCount != null ? saleCount : 0
|
||||
id: p.id ?? '',
|
||||
name: p.name ?? '',
|
||||
price: p.base_price ?? 0,
|
||||
image: p.main_image_url ?? '/static/default.jpg',
|
||||
sales: saleCount != null ? saleCount : 0,
|
||||
merchant_id: p.merchant_id ?? ''
|
||||
}
|
||||
allItems.push(item)
|
||||
}
|
||||
@@ -410,7 +409,8 @@ const loadData = async (): Promise<void> => {
|
||||
|
||||
} catch (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 resultList: Array<SearchResultType> = []
|
||||
for (let i: number = 0; i < prodData.length; i++) {
|
||||
const p = prodData[i] as UTSJSONObject
|
||||
const p = prodData[i] as Product
|
||||
let tag = ''
|
||||
const tagsRaw = p.get('tags')
|
||||
const tagsRaw = p.tags
|
||||
if (tagsRaw != null) {
|
||||
try {
|
||||
const tagsStr = p.getString('tags')
|
||||
const tagsStr = p.tags
|
||||
if (tagsStr != null) {
|
||||
const tags = JSON.parse(tagsStr)
|
||||
const tags = JSON.parse(tagsStr as string)
|
||||
if (Array.isArray(tags) && tags.length > 0) {
|
||||
const firstTag = tags[0]
|
||||
tag = firstTag != null ? (firstTag as string) : ''
|
||||
@@ -534,13 +534,14 @@ const performSearch = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
const searchItem: SearchResultType = {
|
||||
id: p.getString('id') ?? '',
|
||||
name: p.getString('name') ?? '',
|
||||
image: p.getString('main_image_url') ?? '/static/default.jpg',
|
||||
price: p.getNumber('base_price') ?? 0,
|
||||
specification: p.getString('specification') ?? '标准规格',
|
||||
id: p.id ?? '',
|
||||
name: p.name ?? '',
|
||||
image: p.main_image_url ?? '/static/default.jpg',
|
||||
price: p.base_price ?? 0,
|
||||
specification: p.specification ?? '标准规格',
|
||||
tag: tag,
|
||||
sales: p.getNumber('sale_count') ?? 0
|
||||
sales: p.sale_count ?? 0,
|
||||
merchant_id: p.merchant_id ?? ''
|
||||
}
|
||||
resultList.push(searchItem)
|
||||
}
|
||||
@@ -599,21 +600,47 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const onInput = (e: any) => {
|
||||
const eObj = e as UTSJSONObject
|
||||
const detailRaw = eObj.get('detail')
|
||||
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject())
|
||||
const val = detail.getString('value') ?? ''
|
||||
searchKeyword.value = val
|
||||
if (val == '') {
|
||||
showResults.value = false
|
||||
searchSuggestions.value = []
|
||||
return
|
||||
try {
|
||||
let val = ''
|
||||
// 处理 input 事件的不同事件对象格式
|
||||
if (e != null) {
|
||||
// UTSJSONObject 格式 (e.detail.value)
|
||||
if (e instanceof UTSJSONObject) {
|
||||
const eObj = e as UTSJSONObject
|
||||
const detailObj = eObj.get('detail')
|
||||
if (detailObj != null && detailObj instanceof UTSJSONObject) {
|
||||
const detail = detailObj as UTSJSONObject
|
||||
const v = detail.get('value')
|
||||
val = v != null ? (v as string) : ''
|
||||
}
|
||||
} else {
|
||||
// 尝试转换为 UTSJSONObject
|
||||
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
|
||||
const detailObj = eObj.get('detail')
|
||||
if (detailObj != null) {
|
||||
const detail = detailObj as UTSJSONObject
|
||||
const v = detail.get('value')
|
||||
val = v != null ? (v as string) : ''
|
||||
} else {
|
||||
const v = eObj.get('value')
|
||||
val = v != null ? (v as string) : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
searchKeyword.value = val
|
||||
if (val == '') {
|
||||
showResults.value = false
|
||||
searchSuggestions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (suggestTimer > 0) clearTimeout(suggestTimer)
|
||||
suggestTimer = setTimeout(() => {
|
||||
fetchSuggestions(val)
|
||||
}, 300)
|
||||
} catch (err) {
|
||||
console.error('onInput error:', err)
|
||||
}
|
||||
|
||||
if (suggestTimer > 0) clearTimeout(suggestTimer)
|
||||
suggestTimer = setTimeout(() => {
|
||||
fetchSuggestions(val)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
@@ -702,7 +729,8 @@ const loadMore = async (): Promise<void> => {
|
||||
price: p.getNumber('base_price') ?? 0,
|
||||
specification: p.getString('specification') ?? '标准规格',
|
||||
tag: tag,
|
||||
sales: p.getNumber('sale_count') ?? 0
|
||||
sales: p.getNumber('sale_count') ?? 0,
|
||||
merchant_id: p.getString('merchant_id') ?? ''
|
||||
}
|
||||
searchResults.value.push(searchItem)
|
||||
}
|
||||
@@ -738,11 +766,42 @@ const viewShopDetail = (shop: ShopResultType) => {
|
||||
})
|
||||
}
|
||||
|
||||
const addToCart = (product: SearchResultType | GuessItemType) => {
|
||||
uni.showToast({ title: '请选择规格', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
viewProductDetail(product)
|
||||
}, 800)
|
||||
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' })
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
// 无规格,直接加入购物车
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
|
||||
uni.hideLoading()
|
||||
if (success) {
|
||||
uni.showToast({ title: '已添加到购物车', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('添加到购物车异常', e)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '操作异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const openCamera = () => {
|
||||
@@ -1142,64 +1201,62 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.guess-item {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 8px;
|
||||
width: 48%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.guess-img-box {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
background-color: #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guess-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.guess-info {
|
||||
padding: 8px;
|
||||
height: 170px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.guess-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
height: 34px; /* 限制2行高度 */
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.guess-price-row {
|
||||
.guess-bottom {
|
||||
display: flex;
|
||||
align-items: flex-end; /* REPLACED baseline */
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 12px;
|
||||
color: #ff5000;
|
||||
}
|
||||
|
||||
.price-num {
|
||||
font-size: 16px;
|
||||
.guess-price {
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.sales-text {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
.guess-add-btn {
|
||||
width: 24px;
|
||||
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 {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column; /* 垂直排列 */
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48%;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 120px; /* 调整图片高度 */
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
height: 170px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 13px; /* 减小字号 */
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
height: 34px; /* 限制高度 */
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.product-tags-row {
|
||||
margin-top: 2px;
|
||||
display: none; /* 隐藏标签以保持简洁 */
|
||||
}
|
||||
|
||||
.product-spec {
|
||||
display: none; /* 隐藏规格 */
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center; /* 垂直居中 */
|
||||
margin-top: 4px;
|
||||
align-items: center;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.price-box {
|
||||
.product-price {
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 16px; /* 减小价格字号 */
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.add-cart-btn {
|
||||
.product-add-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #4CAF50;
|
||||
background-color: #ff5000;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
.add-icon {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,16 +49,11 @@
|
||||
<view class="product-grid">
|
||||
<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" />
|
||||
<view class="product-info">
|
||||
<text class="product-name">{{ product.name }}</text>
|
||||
<view class="price-row">
|
||||
<view class="price-left">
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<text class="product-sales">已售 {{ product.sales }}</text>
|
||||
</view>
|
||||
<view class="cart-btn" @click.stop="addToCart(product)">
|
||||
<text class="cart-icon">🛒</text>
|
||||
</view>
|
||||
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||||
<view class="product-bottom">
|
||||
<text class="product-price">¥{{ product.price }}</text>
|
||||
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -70,9 +65,20 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
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'
|
||||
|
||||
// 优惠券类型定义
|
||||
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 pageSize = ref(6) // 默认显示六个
|
||||
@@ -95,49 +101,50 @@ const merchant = ref<MerchantType>({
|
||||
created_at: ''
|
||||
} as MerchantType)
|
||||
|
||||
const products = ref<Array<ProductType>>([])
|
||||
const isFollowed = ref<boolean>(false)
|
||||
const coupons = ref<Array<CouponTemplateType>>([])
|
||||
const isRefresherTriggered = ref<boolean>(false)
|
||||
const products = ref<ProductType[]>([])
|
||||
const isFollowed = ref(false)
|
||||
const coupons = ref<CouponType[]>([]) // 新增优惠券
|
||||
const isRefresherTriggered = ref(false)
|
||||
|
||||
const checkFollowStatus = async (shopId: string): Promise<void> => {
|
||||
// 函数定义必须在 onMounted 之前
|
||||
// checkFollowStatus 必须在 loadShopData 之前定义
|
||||
const checkFollowStatus = async (shopId: string) => {
|
||||
const userId = supabaseService.getCurrentUserId()
|
||||
if (userId != null && userId !== '') {
|
||||
if (userId != null && userId != '') {
|
||||
try {
|
||||
// @ts-ignore
|
||||
isFollowed.value = await supabaseService.isShopFollowed(shopId, userId)
|
||||
} 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)
|
||||
const shop = await supabaseService.getShopByMerchantId(id)
|
||||
|
||||
if (shop != null) {
|
||||
console.log('Shop loaded successfully:', shop.shop_name)
|
||||
const shopObj = shop as UTSJSONObject
|
||||
// 使用显式类型转换
|
||||
const merchantData: MerchantType = {
|
||||
id: shopObj.getString('id') ?? '',
|
||||
user_id: shopObj.getString('merchant_id') ?? '',
|
||||
shop_name: shopObj.getString('shop_name') ?? '',
|
||||
shop_logo: shopObj.getString('shop_logo') ?? '/static/default-shop.png',
|
||||
shop_banner: shopObj.getString('shop_banner') ?? '/static/default-banner.png',
|
||||
shop_description: shopObj.getString('description') ?? '',
|
||||
contact_name: shopObj.getString('contact_name') ?? '',
|
||||
contact_phone: shopObj.getString('contact_phone') ?? '',
|
||||
id: shop.id,
|
||||
user_id: shop.merchant_id,
|
||||
shop_name: shop.shop_name,
|
||||
shop_logo: shop.shop_logo != null ? shop.shop_logo : '/static/default-shop.png',
|
||||
shop_banner: shop.shop_banner != null ? shop.shop_banner : '/static/default-banner.png',
|
||||
shop_description: shop.description != null ? shop.description : '',
|
||||
contact_name: shop.contact_name != null ? shop.contact_name : '',
|
||||
contact_phone: shop.contact_phone != null ? shop.contact_phone : '',
|
||||
shop_status: 1,
|
||||
rating: shopObj.getNumber('rating_avg') ?? 5.0,
|
||||
total_sales: shopObj.getNumber('total_sales') ?? 0,
|
||||
created_at: shopObj.getString('created_at') ?? ''
|
||||
} as MerchantType
|
||||
rating: shop.rating_avg != null ? shop.rating_avg : 5.0,
|
||||
total_sales: shop.total_sales != null ? shop.total_sales : 0,
|
||||
created_at: shop.created_at != null ? shop.created_at : ''
|
||||
}
|
||||
merchant.value = merchantData
|
||||
|
||||
const shopId = shopObj.getString('id') ?? ''
|
||||
if (shopId !== '') {
|
||||
checkFollowStatus(shopId)
|
||||
}
|
||||
// 检查关注状态
|
||||
checkFollowStatus(shop.id)
|
||||
} else {
|
||||
console.warn('Shop data is null for ID:', id)
|
||||
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 {
|
||||
const rawCoupons = await supabaseService.fetchShopCoupons(id)
|
||||
if (rawCoupons != null && Array.isArray(rawCoupons)) {
|
||||
const couponList: Array<CouponTemplateType> = []
|
||||
for (let i: number = 0; i < rawCoupons.length; i++) {
|
||||
const c = rawCoupons[i] as UTSJSONObject
|
||||
const coupon: CouponTemplateType = {
|
||||
id: c.getString('id') ?? '',
|
||||
name: c.getString('name') ?? '',
|
||||
description: c.getString('description'),
|
||||
coupon_type: c.getNumber('coupon_type') ?? 0,
|
||||
discount_type: c.getNumber('discount_type') ?? 0,
|
||||
discount_value: c.getNumber('discount_value') ?? 0,
|
||||
min_order_amount: c.getNumber('min_order_amount') ?? 0,
|
||||
max_discount_amount: c.getNumber('max_discount_amount'),
|
||||
total_quantity: c.getNumber('total_quantity'),
|
||||
per_user_limit: c.getNumber('per_user_limit') ?? 1,
|
||||
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)
|
||||
// @ts-ignore
|
||||
const res = await supabaseService.fetchShopCoupons(id)
|
||||
if (res != null && Array.isArray(res)) {
|
||||
const couponList: CouponType[] = []
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i]
|
||||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||
couponList.push({
|
||||
id: itemObj.getString('id') ?? '',
|
||||
discount_value: itemObj.getNumber('discount_value') ?? 0,
|
||||
min_order_amount: itemObj.getNumber('min_order_amount') ?? 0,
|
||||
name: itemObj.getString('name') ?? '',
|
||||
start_time: itemObj.getString('start_time') ?? '',
|
||||
end_time: itemObj.getString('end_time') ?? '',
|
||||
status: itemObj.getNumber('status') ?? 1
|
||||
} as CouponType)
|
||||
}
|
||||
coupons.value = couponList
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('SupabaseService.fetchShopCoupons method missing.')
|
||||
} catch(e1) {
|
||||
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
|
||||
isLoading.value = true
|
||||
|
||||
// 保存当前使用的MerchantID,供下拉/触底使用
|
||||
if (currentPage.value === 1) {
|
||||
currentMerchantId.value = id
|
||||
}
|
||||
@@ -197,50 +194,51 @@ const loadShopProducts = async (id: string): Promise<void> => {
|
||||
|
||||
let res: any = {}
|
||||
try {
|
||||
// @ts-ignore
|
||||
res = await supabaseService.getProductsByMerchantId(id, currentPage.value, pageSize.value)
|
||||
} catch(e) {
|
||||
console.error('getProductsByMerchantId missing or error:', e)
|
||||
console.error('getProductsByMerchantId missing or error', e)
|
||||
isLoading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
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) {
|
||||
const list: Array<ProductType> = []
|
||||
|
||||
for (let idx: number = 0; idx < rawList.length; idx++) {
|
||||
const item = rawList[idx] as UTSJSONObject
|
||||
const images: Array<string> = []
|
||||
const list = rawList.map((item: any): ProductType => {
|
||||
// 解析图片数组
|
||||
let images: string[] = []
|
||||
|
||||
const mainImageUrl = item.getString('main_image_url')
|
||||
if (mainImageUrl != null && mainImageUrl !== '') {
|
||||
// 转换为 UTSJSONObject 安全访问属性
|
||||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||
|
||||
// 1. 尝试 main_image_url
|
||||
const mainImageUrl = itemObj.getString('main_image_url')
|
||||
if (mainImageUrl != null && mainImageUrl != '') {
|
||||
images.push(mainImageUrl)
|
||||
}
|
||||
|
||||
const imageUrlsRaw = item.get('image_urls')
|
||||
if (imageUrlsRaw != null) {
|
||||
// 2. 尝试 image_urls (如果 main 为空,或者需要展示多图)
|
||||
const imageUrls = itemObj.get('image_urls')
|
||||
if (imageUrls != null) {
|
||||
try {
|
||||
if (Array.isArray(imageUrlsRaw)) {
|
||||
const arr = imageUrlsRaw as Array<string>
|
||||
if (Array.isArray(imageUrls)) {
|
||||
const arr = imageUrls as string[]
|
||||
if (arr.length > 0) {
|
||||
if (images.length == 0) {
|
||||
for (let i: number = 0; i < arr.length; i++) {
|
||||
images.push(arr[i])
|
||||
}
|
||||
}
|
||||
if (images.length == 0) images.push(...arr)
|
||||
}
|
||||
} else {
|
||||
const rawUrlStr = imageUrlsRaw as string
|
||||
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 (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 {
|
||||
if (images.indexOf(rawUrlStr) === -1) images.push(rawUrlStr)
|
||||
if (images.indexOf(rawUrl) === -1) images.push(rawUrl)
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -248,84 +246,146 @@ const loadShopProducts = async (id: string): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
// 没有任何图片则使用默认
|
||||
if (images.length === 0) {
|
||||
images.push('/static/default-product.png')
|
||||
}
|
||||
|
||||
let safePrice = item.getNumber('base_price')
|
||||
if (safePrice == null) {
|
||||
const p = item.getNumber('price')
|
||||
safePrice = p != null ? p : 0
|
||||
}
|
||||
|
||||
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') ?? '',
|
||||
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: safePrice,
|
||||
original_price: safeMarketPrice,
|
||||
stock: safeStock,
|
||||
sales: safeSales,
|
||||
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: item.getString('created_at') ?? ''
|
||||
created_at: itemObj.getString('created_at') ?? ''
|
||||
} as ProductType
|
||||
list.push(product)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if (currentPage.value === 1) {
|
||||
products.value = list
|
||||
products.value = list
|
||||
} else {
|
||||
for (let i: number = 0; i < list.length; i++) {
|
||||
products.value.push(list[i])
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否还有更多
|
||||
if (list.length < pageSize.value) {
|
||||
hasMore.value = false
|
||||
} else {
|
||||
hasMore.value = true
|
||||
currentPage.value++ // 准备下一页
|
||||
products.value.push(...list)
|
||||
}
|
||||
currentPage.value++
|
||||
hasMore.value = list.length >= pageSize.value
|
||||
} else {
|
||||
console.log('未加载到店铺商品 (本页为空)')
|
||||
if (currentPage.value === 1) {
|
||||
products.value = []
|
||||
}
|
||||
hasMore.value = false
|
||||
hasMore.value = false
|
||||
}
|
||||
|
||||
|
||||
isLoading.value = false
|
||||
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()
|
||||
if (userId == null) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
@@ -338,32 +398,25 @@ const toggleFollow = async (): Promise<void> => {
|
||||
|
||||
uni.showLoading({ title: '处理中' })
|
||||
|
||||
// @ts-ignore
|
||||
if (isFollowed.value) {
|
||||
// 取消关注
|
||||
try {
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.unfollowShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = false
|
||||
uni.showToast({ title: '已取消关注', icon: 'none' })
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch(e) {
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.unfollowShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = false
|
||||
uni.showToast({ title: '已取消关注', icon: 'none' })
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
// 关注
|
||||
try {
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.followShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = true
|
||||
uni.showToast({ title: '关注成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '关注失败', icon: 'none' })
|
||||
}
|
||||
} catch(e) {
|
||||
// @ts-ignore
|
||||
const success = await supabaseService.followShop(shopId, userId)
|
||||
if (success) {
|
||||
isFollowed.value = true
|
||||
uni.showToast({ title: '关注成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '关注失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
@@ -387,20 +440,50 @@ const contactService = () => {
|
||||
}
|
||||
|
||||
const addToCart = async (product: ProductType) => {
|
||||
uni.showLoading({ title: '添加中...' })
|
||||
uni.showLoading({ title: '检查商品...' })
|
||||
|
||||
const success = await supabaseService.addToCart(product.id, 1, '')
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
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: '添加中...' })
|
||||
const success = await supabaseService.addToCart(product.id, 1, '', merchantId)
|
||||
uni.hideLoading()
|
||||
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '添加失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('添加到购物车异常', e)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '已添加到购物车',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '添加失败,请重试',
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -411,89 +494,6 @@ const goToProduct = (id: string) => {
|
||||
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>
|
||||
|
||||
<style>
|
||||
@@ -704,83 +704,68 @@ onReachBottom(() => {
|
||||
}
|
||||
|
||||
.product-item {
|
||||
width: 48%; /* Fallback for calc(50% - 5px) */
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: 48%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-overflow: ellipsis;
|
||||
lines: 2;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
.product-bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
color: #ff4444;
|
||||
font-size: 15px;
|
||||
color: #ff5000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-sales {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.product-add-btn {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
.product-item {
|
||||
width: 32% !important; /* Tablet: 3 items */
|
||||
width: 32% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user