20260227-1

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -200,6 +200,7 @@ type RecommendProduct = {
price: number
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'

View File

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

View File

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

View File

@@ -1394,3 +1394,598 @@ getCurrentUserId 函数 - 将可选链替换为显式 null 检查和 UTSJSONObje
================================================================================
文档结束
================================================================================
================================================================================
二十四、2026-02-27 函数可选参数限制(重要)
================================================================================
1. 可选参数不能跳过传递
- UTS Android 不支持跳过可选参数传递
- 如果函数有多个可选参数,必须按顺序传递所有参数
- 错误示例:
```typescript
// 函数定义
async addToCart(productId: string, quantity: number = 1, skuId?: string, merchantId?: string): Promise<boolean>
// 错误调用 - 跳过了 merchantId 参数
await supabaseService.addToCart(productId, 1, '')
// 编译错误No value passed for parameter 'merchantId'
```
- 正确示例:
```typescript
// 方案1给可选参数添加默认值
async addToCart(productId: string, quantity: number = 1, skuId: string = '', merchantId: string = ''): Promise<boolean>
// 方案2调用时传递所有参数
await supabaseService.addToCart(productId, 1, '', '')
```
2. 可选参数定义规范
- 推荐使用 `param: Type = defaultValue` 而非 `param?: Type`
- `param?: Type` 在 Android 端调用时仍需传递参数
- `param: Type = defaultValue` 可以在不传参时使用默认值
- 示例:
```typescript
// 不推荐 - 调用时仍需传递参数
function foo(a: string, b?: string, c?: string): void
// 推荐 - 可以跳过参数使用默认值
function foo(a: string, b: string = '', c: string = ''): void
```
3. 编译错误提示
- 错误信息:"No value passed for parameter 'xxx'"
- 原因:可选参数在 Android 端不能跳过
- 解决:
1. 修改函数签名,使用默认值 `param: Type = defaultValue`
2. 调用时传递所有参数
4. 最佳实践
- 对于有多个可选参数的函数,统一使用默认值语法
- 调用时显式传递所有参数,避免依赖可选参数跳过
- 在服务层函数定义中,优先使用 `= ''` 或 `= 0` 等默认值
================================================================================
二十五、2026-02-27 模板中的非空断言限制(重要)
================================================================================
1. 模板中不支持非空断言操作符 `!`
- UTS Android 模板中不能使用 `variable!` 非空断言
- 错误示例:
```html
<text v-if="product.original_price != null && product.original_price! > product.price">
```
- 正确示例:
```html
<text v-if="product.original_price != null && product.original_price > product.price">
```
2. 编译错误提示
- 错误信息:"参数类型不匹配:实际类型为 'Number?',预期类型为 'Number'"
- 原因:模板中使用非空断言 `!` 不被支持
- 解决:移除非空断言 `!`,直接使用变量进行比较
3. 最佳实践
- 在模板中,先用 `!= null` 判断可空类型,然后直接使用变量
- UTS 编译器会在 `!= null` 判断后自动识别变量为非空类型
================================================================================
二十六、2026-02-27 未导入类型的处理(重要)
================================================================================
1. 未导入的类型不能直接使用
- 在页面中使用的类型必须先导入或使用 UTSJSONObject 替代
- 错误示例:
```typescript
// Shop 类型未导入
const s = shopRespData[i] as Shop
const id = s.id // 找不到名称 "id"
```
- 正确示例:
```typescript
// 使用 UTSJSONObject
const s = shopRespData[i] as UTSJSONObject
const id = s.getString('id') ?? ''
const name = s.getString('shop_name') ?? ''
```
2. 编译错误提示
- 错误信息:"找不到名称 'XXX'"
- 原因:类型未导入或类型定义不存在
- 解决:
1. 导入需要的类型:`import { Shop } from '@/utils/supabaseService.uts'`
2. 使用 UTSJSONObject 替代:`as UTSJSONObject` 然后用 `getString()`、`getNumber()` 访问属性
3. 最佳实践
- 对于简单的数据转换,推荐使用 UTSJSONObject
- 避免在多个文件中重复定义相同的类型
- 如果需要类型安全,从服务层导入类型定义
================================================================================
二十七、2026-02-27 服务层数据字段完整性(重要)
================================================================================
1. 服务层返回数据必须包含所有必要字段
- 从数据库获取数据时,必须正确映射所有需要的字段
- 错误示例:
```typescript
const product: Product = {
id: prodObj.getString('id') ?? '',
name: prodObj.getString('name') ?? '',
// 错误merchant_id 硬编码为空字符串
merchant_id: ''
} as Product
```
- 正确示例:
```typescript
const product: Product = {
id: prodObj.getString('id') ?? '',
name: prodObj.getString('name') ?? '',
// 正确:从数据库获取 merchant_id
merchant_id: prodObj.getString('merchant_id') ?? ''
} as Product
```
2. 调用服务层方法时必须传递完整参数
- 页面调用服务层方法时,需要传递所有必要参数
- 错误示例:
```typescript
// 错误merchant_id 传空字符串
await supabaseService.addToCart(productId, 1, '', '')
```
- 正确示例:
```typescript
// 正确:从商品对象获取 merchant_id
const merchantId = product.merchant_id ?? ''
await supabaseService.addToCart(productId, 1, '', merchantId)
```
3. 编译错误提示
- 问题表现:数据添加到数据库失败,或添加的数据不完整
- 原因:服务层或页面层缺少必要字段的传递
- 解决:
1. 检查服务层数据映射是否完整
2. 检查页面调用时是否传递了所有必要参数
4. 最佳实践
- 服务层方法返回的对象应包含数据库视图的所有字段
- 页面调用服务层方法时,应从数据对象中获取并传递所有参数
- 对于关联数据(如 merchant_id确保在数据加载时一并获取
================================================================================
二十八、2026-02-27 模板中的非运算符限制(重要)
================================================================================
1. 模板中不支持 `!` 非运算符
- UTS Android 模板中不能使用 `!variable` 非运算符
- 错误示例:
```html
<view v-if="!brand.logo_url">
```
- 正确示例:
```html
<view v-if="brand.logo_url == null || brand.logo_url == ''">
```
2. 编译错误提示
- 错误信息:"找不到名称'not'"
- 原因:模板中不支持非运算符 `!`
- 解决:使用显式的比较表达式替代
3. 最佳实践
- 使用 `== null` 或 `== ''` 检查空值
- 使用 `!= null && != ''` 检查非空值
================================================================================
二十九、2026-02-27 索引访问限制(重要)
================================================================================
1. 不支持 `(obj as any)['key']` 索引访问方式
- UTS Android 不支持对 any 类型使用索引访问
- 错误示例:
```typescript
const detail = (e as any)['detail']
val = detail['value'] ?? ''
```
- 正确示例:
```typescript
// 方案1使用 UTSJSONObject
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
const detail = eObj.get('detail') as UTSJSONObject
val = detail.getString('value') ?? ''
// 方案2先判断类型再转换
if (e instanceof UTSJSONObject) {
const eObj = e as UTSJSONObject
const detail = eObj.get('detail') as UTSJSONObject
val = detail.getString('value') ?? ''
}
```
2. 编译错误提示
- 错误信息:"Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch"
- 原因any 类型不支持索引访问
- 解决:转换为 UTSJSONObject 后使用 `.get()` 方法
3. 最佳实践
- 统一使用 UTSJSONObject 处理动态对象
- 使用 `.get()`、`.getString()`、`.getNumber()` 方法访问属性
- 对于复杂对象,先用 `JSON.parse(JSON.stringify(obj))` 转换
================================================================================
三十、2026-02-27 字符串不能直接作为布尔条件(重要)
================================================================================
1. 字符串不能直接作为 if 条件
- UTS Android 不支持将字符串直接作为布尔条件判断
- 错误示例:
```typescript
const paramId = '123'
if (paramId) { // 错误:字符串不能直接作为布尔条件
// ...
}
```
- 正确示例:
```typescript
const paramId = '123'
if (paramId != null && paramId != '') { // 正确:显式判断
// ...
}
```
2. 编译错误提示
- 错误信息:"Condition type mismatch: inferred type is 'String' but 'Boolean' was expected"
- 原因:字符串类型不能直接作为布尔条件
- 解决:使用显式的比较表达式
3. 最佳实践
- 使用 `!= null && != ''` 检查字符串非空
- 使用 `== null || == ''` 检查字符串为空
================================================================================
三十一、2026-02-27 函数定义顺序(重要)
================================================================================
1. 函数必须在调用前定义
- UTS Android 要求函数在调用之前完成定义
- 这与 JavaScript 的函数提升不同
- 错误示例:
```typescript
onMounted(() => {
loadData() // 错误loadData 还未定义
})
const loadData = async () => {
// ...
}
```
- 正确示例:
```typescript
const loadData = async () => {
// ...
}
onMounted(() => {
loadData() // 正确loadData 已定义
})
```
2. 编译错误提示
- 错误信息:"找不到名称'xxx'"
- 原因:函数在调用点之后定义
- 解决:将函数定义移到调用之前
3. 最佳实践
- 将所有函数定义放在生命周期钩子onMounted、onShow 等)之前
- 按依赖关系排序函数定义顺序
================================================================================
三十二、2026-02-27 联合类型属性访问(重要)
================================================================================
1. 联合类型不能直接访问属性
- 当参数类型为联合类型(如 `A | B`)时,不能直接访问属性
- 错误示例:
```typescript
type A = { id: string, name: string }
type B = { id: string, title: string }
const foo = (item: A | B) => {
const id = item.id // 错误:联合类型不能直接访问属性
}
```
- 正确示例:
```typescript
const foo = (item: A | B) => {
// 方案1转换为 UTSJSONObject
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const id = obj.getString('id') ?? ''
// 方案2使用类型守卫
if ('name' in item) {
const id = item.id // 此时类型已收窄为 A
}
}
```
2. 编译错误提示
- 错误信息:"找不到名称'xxx'"
- 原因:联合类型的属性访问受限
- 解决:转换为 UTSJSONObject 或使用类型守卫
3. 最佳实践
- 对于联合类型参数,统一转换为 UTSJSONObject 处理
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
================================================================================
三十三、2026-02-27 any 类型变量不能赋值为 null重要
================================================================================
1. any 类型变量不能赋值为 null
- UTS Android 中 `any` 类型不能赋值为 `null`
- 错误示例:
```typescript
let res: any = null // 错误Null cannot be a value of a non-null type 'Any'
```
- 正确示例:
```typescript
let res: any = {} // 正确:使用空对象
// 或者
let res: any | null = null // 使用联合类型
```
2. 编译错误提示
- 错误信息:"Null cannot be a value of a non-null type 'Any'"
- 原因any 类型不允许 null 值
- 解决:使用空对象 `{}` 或联合类型 `any | null`
================================================================================
三十四、2026-02-27 对象字面量类型推断问题(重要)
================================================================================
1. 对象字面量直接赋值给 ref 可能类型不匹配
- 当对象字面量直接赋值给特定类型的 ref 时,可能报类型不匹配错误
- 错误示例:
```typescript
merchant.value = {
id: shop.id,
user_id: shop.merchant_id,
// ...
} // 错误Assignment type mismatch
```
- 正确示例:
```typescript
// 方案1显式声明类型
const merchantData: MerchantType = {
id: shop.id,
user_id: shop.merchant_id,
// ...
}
merchant.value = merchantData
// 方案2使用 as 类型断言
merchant.value = {
id: shop.id,
user_id: shop.merchant_id,
// ...
} as MerchantType
```
2. 编译错误提示
- 错误信息:"Assignment type mismatch: actual type is '<anonymous>', but 'XXX' was expected"
- 原因:对象字面量被推断为匿名类型
- 解决:显式声明类型或使用类型断言
================================================================================
三十五、2026-02-27 any 类型不能直接访问属性(重要)
================================================================================
1. any 类型参数不能直接访问属性
- 在 map、forEach 等回调中any 类型的参数不能直接访问属性
- 错误示例:
```typescript
const list = rawList.map((item): ProductType => {
const id = item.id // 错误:找不到名称"id"
const name = item.name // 错误:找不到名称"name"
})
```
- 正确示例:
```typescript
const list = rawList.map((item: any): ProductType => {
// 方案1转换为 UTSJSONObject
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const id = itemObj.getString('id') ?? ''
const name = itemObj.getString('name') ?? ''
// 方案2显式标注参数类型并使用索引
// 注意:这种方式在 UTS Android 中也可能有问题
})
```
2. 编译错误提示
- 错误信息:"找不到名称'xxx'"
- 原因any 类型的属性访问受限
- 解决:转换为 UTSJSONObject 后使用 `.getString()` 等方法
================================================================================
三十六、2026-02-27 类型断言不会添加方法(重要)
================================================================================
1. `as UTSJSONObject` 不会给对象添加方法
- 使用 `as UTSJSONObject` 只是类型断言,不会让普通对象获得 `getString` 等方法
- 错误示例:
```typescript
const profileObj = profile as UTSJSONObject
const id = profileObj.getString('user_id') // 运行时错误getString is not a function
```
- 正确示例:
```typescript
// 必须使用 JSON.parse(JSON.stringify()) 进行真正的转换
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
const id = profileObj.getString('user_id') ?? ''
```
2. 运行时错误提示
- 错误信息:"XXX is not a function"
- 原因:类型断言只是编译时行为,不会改变运行时对象的方法
- 解决:使用 `JSON.parse(JSON.stringify())` 进行真正的对象转换
3. 最佳实践
- 对于从 API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
- 使用 `instanceof UTSJSONObject` 检查对象类型
- 不要依赖 `as` 类型断言来添加方法
================================================================================
三十七、2026-02-27 类型必须包含所有必填字段(重要)
================================================================================
1. 创建类型实例时必须包含所有必填字段
- UTS 类型定义中的非可选字段(不带 `?`)都是必填的
- 错误示例:
```typescript
export type ProductType = {
id: string
merchant_id: string // 必填
category_id: string // 必填
name: string
// ...
}
// 错误:缺少 merchant_id、category_id 等必填字段
return {
id: item.id,
name: item.name,
price: item.price
} as ProductType // 运行时错误missing required property
```
- 正确示例:
```typescript
return {
id: itemObj.getString('id') ?? '',
merchant_id: itemObj.getString('merchant_id') ?? '',
category_id: itemObj.getString('category_id') ?? '',
name: itemObj.getString('name') ?? '未知商品',
description: itemObj.getString('description') ?? '',
images: images,
price: itemObj.getNumber('base_price') ?? 0,
original_price: itemObj.getNumber('market_price') ?? 0,
stock: itemObj.getNumber('total_stock') ?? 0,
sales: itemObj.getNumber('sale_count') ?? 0,
status: 1,
created_at: itemObj.getString('created_at') ?? ''
} as ProductType
```
2. 运行时错误提示
- 错误信息:"Failed to construct type, missing required property: xxx"
- 原因:类型定义中有必填字段未提供
- 解决:
1. 检查类型定义,确认所有必填字段
2. 为所有必填字段提供值,即使是空字符串或默认值
3. 最佳实践
- 查看类型定义,确认哪些字段是必填的(不带 `?`
- 使用 `??` 运算符提供默认值
- 对于可选字段,可以不提供或使用 `null`
================================================================================
三十八、2026-02-27 回调函数不能是 async重要
================================================================================
1. API 回调函数不能使用 async 修饰
- uni API 的回调函数(如 showModal 的 success不支持 async 函数
- 错误示例:
```typescript
uni.showModal({
title: '确认',
content: '确定要删除吗?',
success: async (res) => { // 错误:回调函数不能是 async
if (res.confirm) {
const result = await someAsyncFunction()
}
}
})
```
- 正确示例:
```typescript
uni.showModal({
title: '确认',
content: '确定要删除吗?',
success: (res) => {
if (res.confirm) {
// 使用 Promise.then() 代替 await
someAsyncFunction().then((result) => {
// 处理结果
})
}
}
})
```
2. 编译错误提示
- 错误信息:"参数类型不匹配:实际类型为 'Function1<..., UTSPromise<Unit>>',预期类型为 'Function1<..., Unit>?'"
- 原因:回调函数返回 Promise 而非 void
- 解决:使用 `.then()` 代替 `await`
3. 最佳实践
- 在回调函数中使用 `.then()` 处理异步操作
- 将异步逻辑封装为单独的函数,在回调中调用
================================================================================
三十九、2026-02-27 类型转换前必须检查类型(重要)
================================================================================
1. 使用 `as` 类型转换前必须检查实际类型
- 直接使用 `as string` 转换可能导致运行时类型转换异常
- 错误示例:
```typescript
const idVal = item['id']
const id = idVal as string // 错误:如果 idVal 是其他类型会崩溃
```
- 正确示例:
```typescript
const idVal = item['id']
const id = (idVal != null && typeof idVal == 'string') ? (idVal as string) : ''
```
2. 运行时错误提示
- 错误信息:"null cannot be cast to non-null type kotlin.String"
- 错误信息:"java.lang.Boolean cannot be cast to java.lang.String"
- 原因:直接类型转换时,实际类型与目标类型不匹配
- 解决:使用 `typeof` 检查类型后再转换
3. 最佳实践
- 使用 `typeof` 检查类型
- 使用 `!= null` 检查空值
- 提供默认值防止空指针异常
================================================================================
四十、2026-02-27 UTSJSONObject 必须正确转换(重要)
================================================================================
1. `as UTSJSONObject` 不会添加方法
- 从数据库返回的数据需要正确转换为 UTSJSONObject
- 错误示例:
```typescript
const item = rawList[i]
const brandObj = item as UTSJSONObject // 错误brandObj.getString 不存在
```
- 正确示例:
```typescript
const item = rawList[i]
const brandObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
const id = brandObj.getString('id') ?? ''
```
2. 运行时错误提示
- 错误信息:"getString is not a function"
- 原因:对象没有正确转换为 UTSJSONObject
- 解决:使用 `JSON.parse(JSON.stringify())` 进行转换
3. 最佳实践
- 对于从数据库/API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
================================================================================
================================================================================

View File

@@ -8,20 +8,12 @@
</view>
<view 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>

View File

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

View File

@@ -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)}&timestamp=${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)}&timestamp=${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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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