consumer模块完成度95%,检查消费者前端bug并修复

This commit is contained in:
cyh666666
2026-03-09 17:20:59 +08:00
parent 7b5801a72b
commit 2262d1bfd9
128 changed files with 13485 additions and 1670 deletions

View File

@@ -334,10 +334,7 @@ const updateRecommendList = (recommends: Product[]) => {
const refreshRecommend = async () => {
try {
// 递增页码以获取不同的商品
recommendPage.value = recommendPage.value + 1
// 使用 searchProducts 获取不同页码的 6 个产品
// 鷻加随机偏移量, const randomOffset = Math.floor(Math.random() * 1000)
const hotResp = await supabaseService.searchProducts('', recommendPage.value, 6, 'sales')
const recommends = hotResp.data

View File

@@ -986,7 +986,7 @@ const onRefresh = () => {
.message-title {
font-size: 16px;
color: #1a1a1a;
font-weight: 500;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -152,7 +152,7 @@
</view>
</view>
<view class="order-item-content">
<image :src="getOrderMainImage(order)" class="order-item-image" mode="aspectFill" />
<image :src="getOrderMainImage(order)" class="order-item-image" mode="aspectFill" @click.stop="goToProductFromOrder(order)" />
<view class="order-item-info">
<view class="order-title-row">
<text class="order-item-title">{{ getOrderTitle(order) }}</text>
@@ -583,11 +583,9 @@ export default {
}
}
// 获取积分和余额(并行获取
const [balanceResult, points] = await Promise.all([
supabaseService.getUserBalance(),
supabaseService.getUserPoints()
])
// 获取积分和余额(顺序获取UTS不支持Promise.all数组解构
const balanceResult = await supabaseService.getUserBalance()
const points = await supabaseService.getUserPoints()
const balanceValue = balanceResult.getNumber('balance') ?? 0
@@ -1088,6 +1086,25 @@ export default {
})
},
goToProductFromOrder(order: OrderItemType) {
const itemsRaw = order.ml_order_items
if (itemsRaw == null) return
const items = itemsRaw as any[]
if (items.length > 0) {
const firstItem = items[0]
const itemStr = JSON.stringify(firstItem)
const itemParsed = JSON.parse(itemStr)
if (itemParsed == null) return
const itemObj = itemParsed as UTSJSONObject
const productId = itemObj.getString('product_id')
if (productId != null && productId !== '') {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}`
})
}
}
},
payOrder(order: OrderItemType) {
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${order.id}`

View File

@@ -454,7 +454,6 @@ const deleteAddress = () => {
padding: 0 4px;
background-color: transparent; /* 确保输入框背景透明 */
border: none; /* 强制去除安卓原生边框 */
outline: none; /* 强制去除焦点边框 */
}
.textarea {
@@ -466,7 +465,6 @@ const deleteAddress = () => {
padding: 4px 0;
background-color: transparent;
border: none; /* 强制去除安卓原生边框 */
outline: none; /* 强制去除焦点边框 */
}
.placeholder {

View File

@@ -182,7 +182,6 @@ const selectAddress = (item: Address) => {
<style>
.address-list-page {
background-color: #f8f8f8;
min-height: 100vh;
padding: 12px;
padding-bottom: 100px;
}

View File

@@ -99,7 +99,6 @@ const loadRecords = async (): Promise<void> => {
for (let i = 0; i < result.length; i++) {
const item = result[i]
const itemAny = item as any
let id = ''
let type = ''
@@ -109,16 +108,21 @@ const loadRecords = async (): Promise<void> => {
let description: string | null = null
let createdAt = ''
if (typeof itemAny._getValue === 'function') {
id = (itemAny._getValue('id') as string) ?? ''
type = (itemAny._getValue('type') as string) ?? ''
amount = (itemAny._getValue('amount') as number) ?? 0
balanceBefore = (itemAny._getValue('balance_before') as number) ?? 0
balanceAfter = (itemAny._getValue('balance_after') as number) ?? 0
description = itemAny._getValue('description') as string | null
createdAt = (itemAny._getValue('created_at') as string) ?? ''
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
id = itemObj.getString('id') ?? ''
type = itemObj.getString('type') ?? ''
amount = itemObj.getNumber('amount') ?? 0
balanceBefore = itemObj.getNumber('balance_before') ?? 0
balanceAfter = itemObj.getNumber('balance_after') ?? 0
description = itemObj.getString('description')
createdAt = itemObj.getString('created_at') ?? ''
parsed.push({
id,
type,

View File

@@ -1533,7 +1533,7 @@ const goToLogin = () => {
.address-popup {
background-color: #ffffff;
width: 100%;
height: 60vh; /* 显式高度保证内部 scroll-view direction="vertical" 生效 */
height: 450px;
border-radius: 20px 20px 0 0;
display: flex;
flex-direction: column;
@@ -1544,7 +1544,7 @@ const goToLogin = () => {
background-color: #f8f8f8;
width: 92%;
max-width: 500px;
height: 85vh;
height: 600px;
border-radius: 16px;
display: flex;
flex-direction: column;
@@ -1622,7 +1622,7 @@ const goToLogin = () => {
flex-shrink: 0;
}
.popup-btn-icon { color: #ffffff; font-size: 20px; margin-right: 6px; font-weight: 300; }
.popup-btn-icon { color: #ffffff; font-size: 20px; margin-right: 6px; font-weight: normal; }
.popup-btn-text { color: #ffffff; font-size: 15px; font-weight: bold; }
.address-form-popup {
@@ -1684,7 +1684,7 @@ const goToLogin = () => {
font-size: 14px;
color: #333;
margin-bottom: 10px;
display: block;
display: flex;
}
.label-row {
@@ -1740,7 +1740,7 @@ const goToLogin = () => {
}
.checkbox-wrapper { display: flex; flex-direction: row; align-items: center; }
.checkbox { width: 18px; height: 18px; border: 1.5px solid #ddd; border-radius: 50%; margin-right: 10px; display: flex; align-items: center; justify-content: center; }
.checkbox { width: 18px; height: 18px; border: 1.5px solid #ddd; border-radius: 9px; margin-right: 10px; display: flex; align-items: center; justify-content: center; }
.checkbox.checked { background-color: #ff5000; border-color: #ff5000; }
.checkbox-check { color: #ffffff; font-size: 12px; }
.checkbox-label { font-size: 14px; color: #333; }

View File

@@ -109,35 +109,74 @@
1. 布局方式
- 只支持 display: flex
- 不支持 display: grid
- 不支持 display: grid、display: block
- 不支持 gap
- 不支持 table、grid、grid-template-columns
2. 单位与计算
- 不支持 calc()
- 不支持的单位: vh
- 不支持的单位: vh、vw
- property value `100%` is not supported for min-height (supported values are: number|pixel)
- property value `60%` is not supported for max-height (supported values are: number|pixel)
- property value `calc(33.33% - 10px)` is not supported for min-width
- height 不支持 vh 单位,需要使用具体的像素值或百分比
3. 选择器
- [APP-ANDROID] 不支持伪类选择器
- [APP-IOS] 不支持伪类选择器
- ERROR: Selector `.login-button[disabled]` is not supported. uvue only support classname selector
4. 其他样式
4. font-weight 限制
- font-weight 只支持: normal | bold | 400 | 700
- 不支持: 100, 300, 500, 600, 900 等其他数值
5. border-radius 限制
- border-radius 不支持百分比单位 (如 50%)
- 需要使用具体的像素值 (如 border-radius: 9px 实现圆形)
6. line-height 限制
- line-height 不支持 normal 值
- 需要使用具体的数值或像素值 (如 line-height: 36px 或 line-height: 1.5)
7. position 限制
- position 不支持 sticky 值
- 只支持: relative | absolute | fixed
8. 不支持的 CSS 属性
- outline 属性不支持
- aspect-ratio 属性不支持,需要用具体的 width 和 height 值
- text-decoration 属性不支持(如 line-through
- align-items 不支持 baseline 值,只支持 center | flex-start | flex-end | stretch
- 不支持后代选择器(如 .parent text只支持类名选择器
9. 其他样式
- WARNING: `backdrop-filter` is not a standard property name
- style property `white-space` is only supported on `<text>|<button>`
- ERROR: property value `all` is not supported for `transition-property`
================================================================================
六、scroll-view 使用
六、UTS 模板表达式限制
================================================================================
1. 条件语句必须使用布尔类型
- 错误码: UTS110111120
- 不支持 || 运算符的隐式类型转换
- 错误写法: {{ value || '默认值' }}
- 正确写法: {{ value != null && value != '' ? value : '默认值' }}
2. 模板中的类型判断
- 所有条件表达式必须返回布尔值
- 不能使用 truthy/falsy 值作为条件
================================================================================
七、scroll-view 使用
================================================================================
- scroll-view 在 uni-app-x 中不是用 scroll-y=true
- 而是要用 direction="vertical"
================================================================================
、异步与回调
、异步与回调
================================================================================
- uni.showModal 的 success 回调不能是 async 函数
@@ -146,7 +185,7 @@
- 解决方案:使用普通函数 function name(): Type {} 代替箭头函数
================================================================================
、响应式数据
、响应式数据
================================================================================
- 对于需要整体替换的数组,推荐使用 ref 而非 reactive
@@ -155,7 +194,7 @@
- 对于可能为 null 的参数,需要显式检查后再传递给函数
================================================================================
、类型导入
、类型导入
================================================================================
- 类型导入需要使用 type 关键字
@@ -165,7 +204,7 @@
- 返回的是 resultresultdata 一般可以 as Array<T>
================================================================================
十、常见错误速查
、常见错误速查
================================================================================
1. "Unresolved reference" - 函数未定义或顺序错误
@@ -179,7 +218,7 @@
9. "参数类型不匹配" - 参数类型错误,需要显式类型转换
================================================================================
、简明速记100+条)
、简明速记100+条)
================================================================================
1. 表单优先用 form 组件
@@ -197,84 +236,89 @@
13. 类型推断严格,必要时用 as Type 明确类型
14. 不支持 Intersection Type
15. picker 用 picker-view 或 uni.showActionSheet 替代
16. 样式只支持 display: flex不支持 gap、grid、calc()、伪类选择器
16. 样式只支持 display: flex不支持 gap、grid、calc()、伪类选择器、display: block
17. scroll-view 用 direction="vertical"
18. 不支持 table、grid、vh 单位、min-width: 100%
19. 组件事件如 picker-view 用 UniPickerViewChangeEvent
20. 时间选择用 uni_modules/lime-date-time-picker
21. 类型转换建议用 utils/utis 下的 UTSJSONObject
22. 在 uts setup 的 android 模式下,调用的函数必须在调用之前定义
23. 箭头函数不支持默认参数值,改用显式传参或普通函数定义
24. 不支持 Number()、String() 构造函数,用 as 类型转换
25. 不支持 Object.keys(),用 JSON.stringify() 或 for 循环
26. 不支持 typeof xxx === 'function',用 try-catch 替代
27. 不支持 as unknown as 语法
28. parseInt() 参数必须是 string 类型
29. decodeURIComponent() 返回可空类型,需要处理 null
30. charCodeAt() 返回可空类型,需要处理 null
31. 不支持内联对象类型,需要在 types 文件中单独定义
32. UTSJSONObject.get() 返回可空类型 Any?,需要处理 null 并转换为具体类型
33. Array<any> 元素不能直接访问属性,需转换为 UTSJSONObject 或定义明确类型
34. 类型定义中属性可能为 null 时,必须声明为可空类型(如 any | null
35. switch 语句在某些版本可能有问题,建议用 if-else 替代
36. 模板中可选链 ?.length 需要改为显式判断v-if="arr != null && arr.length > 0"
37. 解构赋值 const { data, error } 在 UTS 中可能有问题,建议用 response.data 方式访问
38. response.data 返回 Any?,赋值前需要判断 null 并类型转换
39. 空数组 [] 无法推断类型需要显式声明let arr: Array<any> = [] 或先判断 null 再转换
40. ref 对象字面量需要定义类型const obj = ref<MyType>({...} as MyType),否则属性访问会报错
41. if 条件必须是 boolean可空类型要用 != null 判断if (obj != null) 而非 if (obj)
42. throw 语句不能抛出 Any 类型,需要处理错误而非抛出
43. 模板中可选链 ?.property 需要改为三元表达式obj != null ? obj.property : ''
44. ref<any> 在模板中无法访问属性必须定义明确类型ref<MyType | null>(null)
45. 展开运算符 [...arr] 不支持,需要手动复制数组
46. Array.from(new Set()) 不支持,需要手动去重
47. Promise.all 可能有问题,建议改为顺序执行
48. .sort(() => Math.random() - 0.5) 随机排序不支持,需要手动实现
49. 数组索引访问 arr[index] 可能越界,建议用 if-else 替代数组查找
50. 事件对象 e.detail.value 需要转换为 UTSJSONObject 后访问
51. String() 构造函数不支持,用 as string 类型转换
52. 数组类型简写 string[] 需要改为 Array<string>
53. any 类型参数不能直接访问属性,需要转换为 UTSJSONObject 后使用 get/getString/getNumber 方法
54. 模板中 !变量 取反不支持改为显式判断v-if="str == ''" 或 v-if="bool == false"
55. 模板中 :class="{ 'class': condition }" 对象语法可能有问题,改为三元表达式::class="condition ? 'class' : ''"
56. supabase .update() 参数需要 UTSJSONObject 类型,用 new UTSJSONObject() 创建并用 .set() 设置属性
57. ref<Array<any>> 在模板中无法访问元素属性,必须定义明确的类型后才能访问
58. 函数参数可以用联合类型func(item: TypeA | TypeB)
59. JSON.stringify(UTSJSONObject) 可能有问题,需要手动拼接字符串
60. UTSJSONObject.keys() 方法不存在,无法获取键列表
61. 联合类型参数不能直接访问属性需要先类型转换const id = (item as TypeA).id
62. 某些 uni API 可能不存在(如 navigateToMiniProgram需要检查或替换
63. JSON.parse(JSON.stringify(obj)) 复杂转换可能有问题,简化处理
64. showModal success 回调不能是 async 函数,需要改为同步或使用 Promise
65. supa.auth.signOut() 等 supabase auth 方法可能不支持,需要简化处理
66. 模板中可空字符串判断 userInfo.phone ? 改为 userInfo.phone != null && userInfo.phone != ''
67. Promise.all() 可能有问题,建议改为顺序执行或 setTimeout
68. .then() 回调可能有问题,建议用 async/await 或直接调用
69. let res: any = null 不支持,改为 let res: any = {} 或其他默认值
70. 类型定义中没有的字段不能赋值,检查类型定义后移除多余字段
71. ref<Array<any>> 在模板中无法访问元素属性,必须定义明确的类型
72. 模板中复杂表达式如 parseFloat(String(x)) 不支持,简化为直接比较
73. forEach 不支持,改用 for 循环
74. any 类型数组元素不能直接访问属性,需转换为 UTSJSONObject
75. showModal success 回调不能是 async 函数,需要改为同步调用独立 async 函数
76. 被生命周期钩子调用的函数必须在钩子之前定义,包括 onMounted、watch、onUnmounted 等
77. 箭头函数不支持默认参数值,改用显式传参或普通函数定义
78. 对象字面量赋值给 ref<Type> 需要显式类型声明const obj: Type = {...} as Type
79. 模板中访问对象属性时,类型定义必须包含该属性,否则报 "找不到名称" 错误
80. !variable 取反操作不支持,改为 variable == '' 或 variable == false
81. supa.auth 方法不支持,需要简化处理或移除
82. setInterval 回调中使用外部变量需要先声明let timer: number = 0然后在回调中赋值
83. $t() 国际化函数在模板中可能有问题,建议使用硬编码文本或自定义翻译函数
84. profile.username ?? $t('xxx') 混合表达式不支持改为条件判断profile != null && profile.username != null ? profile.username : '默认值'
85. 可选链操作符 ?. 在某些场景不支持,如 currentPage?.options需要改为 if 判断
86. as any[] 类型转换后无法访问属性,需要使用正确的类型如 UTSJSONObject
87. 可空类型 string | null 传给需要 string 的函数需要显式类型转换redirect as string
88. setInterval 回调中修改外部变量,需要用 ref 而不是 let 声明变量,避免 smart cast 问题
89. 非空断言操作符 ! 在某些场景仍无法解决类型问题,建议简化逻辑避免复杂类型转换
90. decodeURIComponent 函数参数类型严格,可空类型即便使用 ! 也可能报错,建议简化或避免使用
91. getCurrentPages() 获取页面 options 复杂且容易出错,建议简化跳转逻辑
92. 模板中内联箭头函数不支持类型注解,如 @input="(e: any) => ..." 会报错,改用 v-model
93. :class="{ disabled: codeDisabled }" 对象语法可能有问题,改为三元表达式 :class="codeDisabled ? 'disabled' : ''"
18. 不支持 table、grid、vh/vw 单位、min-height/max-height 百分比
19. font-weight 只支持 normal/bold/400/700不支持 300/500/600 等数值
20. border-radius 不支持百分比(50%),用具体像素值(9px)实现圆形
21. line-height 不支持 normal用具体数值(36px)或倍数(1.5)
22. position 不支持 sticky只支持 relative/absolute/fixed
23. 不支持 outline、aspect-ratio、text-decoration 等 CSS 属性
24. align-items 不支持 baseline只支持 center/flex-start/flex-end/stretch
25. 组件事件如 picker-view 用 UniPickerViewChangeEvent
26. 时间选择用 uni_modules/lime-date-time-picker
27. 类型转换建议用 utils/utis 下的 UTSJSONObject
28. 在 uts setup 的 android 模式下,调用的函数必须在调用之前定义
29. 箭头函数不支持默认参数值,改用显式传参或普通函数定义
30. 不支持 Number()、String() 构造函数,用 as 类型转换
31. 不支持 Object.keys(),用 JSON.stringify() 或 for 循环
32. 不支持 typeof xxx === 'function',用 try-catch 替代
33. 不支持 as unknown as 语法
34. parseInt() 参数必须是 string 类型
35. decodeURIComponent() 返回可空类型,需要处理 null
36. charCodeAt() 返回可空类型,需要处理 null
37. 不支持内联对象类型,需要在 types 文件中单独定义
38. UTSJSONObject.get() 返回可空类型 Any?,需要处理 null 并转换为具体类型
39. Array<any> 元素不能直接访问属性,需转换为 UTSJSONObject 或定义明确类型
40. 类型定义中属性可能为 null 时,必须声明为可空类型(如 any | null
41. switch 语句在某些版本可能有问题,建议用 if-else 替代
42. 模板中可选链 ?.length 需要改为显式判断v-if="arr != null && arr.length > 0"
43. 解构赋值 const { data, error } 在 UTS 中可能有问题,建议用 response.data 方式访问
44. response.data 返回 Any?,赋值前需要判断 null 并类型转换
45. 空数组 [] 无法推断类型需要显式声明let arr: Array<any> = [] 或先判断 null 再转换
46. ref 对象字面量需要定义类型const obj = ref<MyType>({...} as MyType),否则属性访问会报错
47. if 条件必须是 boolean可空类型要用 != null 判断if (obj != null) 而非 if (obj)
48. throw 语句不能抛出 Any 类型,需要处理错误而非抛出
49. 模板中可选链 ?.property 需要改为三元表达式obj != null ? obj.property : ''
50. ref<any> 在模板中无法访问属性必须定义明确类型ref<MyType | null>(null)
51. 展开运算符 [...arr] 不支持,需要手动复制数组
52. Array.from(new Set()) 不支持,需要手动去重
53. Promise.all 可能有问题,建议改为顺序执行
54. .sort(() => Math.random() - 0.5) 随机排序不支持,需要手动实现
55. 数组索引访问 arr[index] 可能越界,建议用 if-else 替代数组查找
56. 事件对象 e.detail.value 需要转换为 UTSJSONObject 后访问
57. String() 构造函数不支持,用 as string 类型转换
58. 数组类型简写 string[] 需要改为 Array<string>
59. any 类型参数不能直接访问属性,需要转换为 UTSJSONObject 后使用 get/getString/getNumber 方法
60. 模板中 !变量 取反不支持改为显式判断v-if="str == ''" 或 v-if="bool == false"
61. 模板中 :class="{ 'class': condition }" 对象语法可能有问题,改为三元表达式::class="condition ? 'class' : ''"
62. supabase .update() 参数需要 UTSJSONObject 类型,用 new UTSJSONObject() 创建并用 .set() 设置属性
63. ref<Array<any>> 在模板中无法访问元素属性,必须定义明确的类型后才能访问
64. 函数参数可以用联合类型func(item: TypeA | TypeB)
65. JSON.stringify(UTSJSONObject) 可能有问题,需要手动拼接字符串
66. UTSJSONObject.keys() 方法不存在,无法获取键列表
67. 联合类型参数不能直接访问属性需要先类型转换const id = (item as TypeA).id
68. 某些 uni API 可能不存在(如 navigateToMiniProgram需要检查或替换
69. JSON.parse(JSON.stringify(obj)) 复杂转换可能有问题,简化处理
69. showModal success 回调不能是 async 函数,需要改为同步或使用 Promise
70. supa.auth.signOut() 等 supabase auth 方法可能不支持,需要简化处理
71. 模板中可空字符串判断 userInfo.phone ? 改为 userInfo.phone != null && userInfo.phone != ''
72. .then() 回调可能有问题,建议用 async/await 或直接调用
73. let res: any = null 不支持,改为 let res: any = {} 或其他默认值
74. 类型定义中没有的字段不能赋值,检查类型定义后移除多余字段
75. ref<Array<any>> 在模板中无法访问元素属性,必须定义明确的类型
76. 模板中复杂表达式如 parseFloat(String(x)) 不支持,简化为直接比较
77. forEach 不支持,改用 for 循环
78. any 类型数组元素不能直接访问属性,需转换为 UTSJSONObject
79. showModal success 回调不能是 async 函数,需要改为同步调用独立 async 函数
80. 被生命周期钩子调用的函数必须在钩子之前定义,包括 onMounted、watch、onUnmounted 等
81. 箭头函数不支持默认参数值,改用显式传参或普通函数定义
82. 对象字面量赋值给 ref<Type> 需要显式类型声明const obj: Type = {...} as Type
83. 模板中访问对象属性时,类型定义必须包含该属性,否则报 "找不到名称" 错误
84. !variable 取反操作不支持,改为 variable == '' 或 variable == false
85. supa.auth 方法不支持,需要简化处理或移除
86. setInterval 回调中使用外部变量需要先声明let timer: number = 0然后在回调中赋值
87. $t() 国际化函数在模板中可能有问题,建议使用硬编码文本或自定义翻译函数
88. profile.username ?? $t('xxx') 混合表达式不支持改为条件判断profile != null && profile.username != null ? profile.username : '默认值'
89. 可选链操作符 ?. 在某些场景不支持,如 currentPage?.options需要改为 if 判断
90. as any[] 类型转换后无法访问属性,需要使用正确的类型如 UTSJSONObject
91. 可空类型 string | null 传给需要 string 的函数需要显式类型转换redirect as string
92. setInterval 回调中修改外部变量,需要用 ref 而不是 let 声明变量,避免 smart cast 问题
93. 非空断言操作符 ! 在某些场景仍无法解决类型问题,建议简化逻辑避免复杂类型转换
94. decodeURIComponent 函数参数类型严格,可空类型即便使用 ! 也可能报错,建议简化或避免使用
95. getCurrentPages() 获取页面 options 复杂且容易出错,建议简化跳转逻辑
96. 模板中内联箭头函数不支持类型注解,如 @input="(e: any) => ..." 会报错,改用 v-model
97. :class="{ disabled: codeDisabled }" 对象语法可能有问题,改为三元表达式 :class="codeDisabled ? 'disabled' : ''"
94. 使用外部类型定义时,确保所有属性都有默认值,避免 null 导致类型不匹配
95. Supabase insert/update 在 .uvue 文件中直接调用可能报类型错误,建议封装到 .uts 服务文件中调用
96. 可空类型属性在模板中使用时需要处理 nullprofile.gender ?? 'other'
@@ -289,7 +333,7 @@
105. 使用辅助函数 safeGetString、safeGetNumber 处理数据库字段
================================================================================
、构造函数限制
、构造函数限制
================================================================================
1. 不支持 Number() 构造函数,使用 as number 类型转换
@@ -297,7 +341,7 @@
3. 示例Number(x) → x as numberString(i) → i as string
================================================================================
、取反操作符限制
、取反操作符限制
================================================================================
1. 不支持 !变量 的取反操作符用于判断空
@@ -305,7 +349,7 @@
3. 示例:!this.selectedSkuId → (this.selectedSkuId == null || this.selectedSkuId === '')
================================================================================
、parseInt/parseFloat 限制
、parseInt/parseFloat 限制
================================================================================
1. parseInt() 参数必须是 string 类型
@@ -313,7 +357,7 @@
3. 示例parseInt(this.quantity) 错误quantity 是 number直接用 this.quantity
================================================================================
、Object.keys() 不支持
、Object.keys() 不支持
================================================================================
1. 不支持 Object.keys() 方法
@@ -321,21 +365,21 @@
3. UTSJSONObject 可用 .keys() 方法(但某些版本可能不支持)
================================================================================
、typeof 函数检查不支持
、typeof 函数检查不支持
================================================================================
1. 不支持 typeof xxx === 'function' 语法
2. 替代方案:使用 try-catch 包裹方法调用
================================================================================
、as unknown as 语法不支持
、as unknown as 语法不支持
================================================================================
1. 不支持 as unknown as 双重类型转换
2. 直接使用 as 目标类型obj as UTSJSONObject
================================================================================
、可空类型方法返回值
、可空类型方法返回值
================================================================================
1. decodeURIComponent() 返回 String?,需要处理 null
@@ -343,7 +387,7 @@
3. 示例const code = str.charCodeAt(i); if (code != null) { ... }
================================================================================
、内联对象类型不支持
十、内联对象类型不支持
================================================================================
1. 不支持 Array<{id: string, name: string}> 这种内联类型定义
@@ -351,28 +395,28 @@
3. 示例:定义 type ItemType = { id: string, name: string },然后使用 Array<ItemType>
================================================================================
二十、eventChannel 不支持
二十、eventChannel 不支持
================================================================================
1. uni.navigateTo 的 success 回调中 res.eventChannel 不支持
2. 替代方案:使用 Storage 或全局变量传递数据
================================================================================
二十、链式调用问题
二十、链式调用问题
================================================================================
1. .map().join() 链式调用可能导致类型推断失败
2. 替代方案:使用 for 循环或分步处理
================================================================================
二十、v-model 类型限制
二十、v-model 类型限制
================================================================================
1. input 的 v-model 期望 string 类型
2. 如果变量是 number使用 :value="variable.toString()" 替代 v-model
================================================================================
二十、setup 模式函数定义顺序(重要)
二十、setup 模式函数定义顺序(重要)
================================================================================
在 <script setup lang="uts"> 中,函数定义顺序至关重要:
@@ -447,7 +491,7 @@
- 解决:调整函数定义顺序,确保被调用的函数先定义
================================================================================
二十、箭头函数限制(重要)
二十、箭头函数限制(重要)
================================================================================
1. 箭头函数不支持默认参数值
@@ -475,7 +519,7 @@
- 解决:移除默认参数,改用显式传参或普通函数定义
================================================================================
二十、数组元素属性访问(重要)
二十、数组元素属性访问(重要)
================================================================================
1. Array<any> 元素属性访问问题
@@ -505,7 +549,7 @@
- 解决:转换为 UTSJSONObject 或定义明确类型
================================================================================
二十、类型定义与可空类型(重要)
二十、类型定义与可空类型(重要)
================================================================================
1. 类型定义中可空类型的处理
@@ -543,7 +587,7 @@
- 解决:修改类型定义为可空类型,或处理 null 情况
================================================================================
二十、模板中的可空类型处理(重要)
二十、模板中的可空类型处理(重要)
================================================================================
1. 模板中可选链限制
@@ -563,7 +607,7 @@
- 解决:显式判断 null 后再访问属性
================================================================================
二十、解构赋值限制(重要)
二十、解构赋值限制(重要)
================================================================================
1. UTS 中解构赋值可能有问题
@@ -583,7 +627,7 @@
- 避免使用解构赋值
================================================================================
二十九、API 响应数据处理(重要)
三十、API 响应数据处理(重要)
================================================================================
1. response.data 返回 Any? 类型
@@ -630,7 +674,7 @@
- 解决:显式声明数组类型
================================================================================
三十、ref 对象字面量类型(重要)
三十、ref 对象字面量类型(重要)
================================================================================
1. ref 对象字面量必须定义类型
@@ -665,7 +709,7 @@
- 解决:定义 type 并在 ref 中指定泛型类型
================================================================================
三十、if 条件与可空类型(重要)
三十、if 条件与可空类型(重要)
================================================================================
1. if 条件必须是 boolean 类型
@@ -695,7 +739,7 @@
- 解决:使用 != null 判断
================================================================================
三十、throw 语句限制(重要)
三十、throw 语句限制(重要)
================================================================================
1. throw 语句不能抛出 Any 类型
@@ -727,7 +771,7 @@
- 解决:处理错误而非抛出,或创建 Error 对象
================================================================================
三十、模板中的可选链与属性访问(重要)
三十、模板中的可选链与属性访问(重要)
================================================================================
1. 模板中可选链限制
@@ -769,7 +813,7 @@
- 解决:定义明确类型,使用三元表达式代替可选链
================================================================================
三十、数组操作限制(重要)
三十、数组操作限制(重要)
================================================================================
1. 展开运算符 [...arr] 不支持

View File

@@ -2,54 +2,108 @@
## 一、功能概述
本文档描述商城消费者端的推销模式功能,包含以下三大模块
1. **分享免单系统** - 用户分享商品,达成条件后免单
2. **会员等级系统** - 用户等级享受不同优惠价格
3. **经销点返利系统** - 经销点销售返利
本文档描述商城消费者端的推销模式功能,**该功能为商家级别功能**,即
- **商家可自主开启/关闭**自己店铺的推销模式
- 只有**开启了推销模式的商家**,其店铺订单才会显示"分享免单"按钮
- **会员等级系统**为全局功能,适用于所有商家
- **经销点返利系统**为商家级别功能,由商家自行配置
### 1.1 核心变更说明
| 功能模块 | 作用范围 | 说明 |
|---------|---------|------|
| 分享免单系统 | 商家级别 | 商家开启后,其订单才显示分享免单入口 |
| 会员等级系统 | 全局 | 所有用户统一等级体系 |
| 经销点返利系统 | 商家级别 | 商家自行创建和管理经销点 |
| 用户余额系统 | 商家级别 | 每个商家有独立的用户余额池 |
---
## 二、分享免单系统
## 二、商家推销模式配置
### 2.1 功能描述
用户购买商品后可分享商品链接给其他用户二级用户。当二级用户通过分享链接购买该商品累计达到指定数量默认4件原用户可获得免单奖励免单金额存入账户余额。
### 2.2 业务流程
```
用户A购买商品 → 生成分享链接 → 分享给用户B/C/D
二级用户通过链接购买
累计购买数量达标4件
用户A获得免单奖励
奖励金额存入余额
商家微信返现后清零余额
```
### 2.3 数据库设计
#### 2.3.1 分享记录表 ml_share_records
### 2.1 商家推销设置表 ml_merchant_promotion_config
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| merchant_id | UUID | 商家ID关联ml_shops.merchant_id |
| promotion_enabled | BOOLEAN | 是否开启推销模式默认false |
| share_free_enabled | BOOLEAN | 是否开启分享免单默认false |
| distribution_enabled | BOOLEAN | 是否开启经销点返利默认false |
| required_count | INT | 分享免单所需购买数默认4 |
| reward_type | VARCHAR(20) | 奖励类型product_price-商品价格fixed-固定金额 |
| fixed_reward_amount | DECIMAL(10,2) | 固定奖励金额reward_type=fixed时使用 |
| created_at | TIMESTAMPTZ | 创建时间 |
| updated_at | TIMESTAMPTZ | 更新时间 |
### 2.2 商家用户余额表 ml_merchant_user_balance
> 每个商家的用户余额独立计算,用户在不同商家有不同余额
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| merchant_id | UUID | 商家ID |
| user_id | UUID | 用户ID |
| balance | DECIMAL(10,2) | 当前余额 |
| frozen_balance | DECIMAL(10,2) | 冻结余额 |
| total_earned | DECIMAL(10,2) | 累计获得 |
| total_withdrawn | DECIMAL(10,2) | 累计提现 |
| updated_at | TIMESTAMPTZ | 更新时间 |
| UNIQUE(merchant_id, user_id) | | 联合唯一约束 |
---
## 三、分享免单系统
### 3.1 功能描述
用户购买商品后,可分享商品链接给其他用户(二级用户)。当二级用户通过分享链接购买该商品累计达到指定数量时,原用户可获得免单奖励。
**重要变更:**
- 只有开启了分享免单功能的商家,其订单才显示分享免单入口
- 分享免单所需购买数由商家自行设置默认4人
- 奖励金额可以是商品价格或固定金额
### 3.2 业务流程
```
商家开启推销模式 → 用户购买商品 → 订单详情显示"分享免单"按钮
用户点击创建分享记录
分享给好友B/C/D
好友通过分享码购买
累计购买数量达标
用户获得免单奖励
奖励存入该商家下的用户余额
```
### 3.3 数据库设计
#### 3.3.1 分享记录表 ml_share_records
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| merchant_id | UUID | 商家ID新增 |
| user_id | UUID | 分享用户ID |
| product_id | UUID | 商品ID |
| order_id | UUID | 关联订单ID |
| share_code | VARCHAR(20) | 分享码(唯一) |
| required_count | INT | 需要的购买数量默认4 |
| required_count | INT | 需要的购买数量 |
| current_count | INT | 当前已购买数量 |
| status | INT | 状态0-进行中1-已完成2-已失效 |
| reward_amount | DECIMAL(10,2) | 奖励金额 |
| created_at | TIMESTAMPTZ | 创建时间 |
| completed_at | TIMESTAMPTZ | 完成时间 |
#### 2.3.2 二级购买记录表 ml_secondary_purchases
#### 3.3.2 二级购买记录表 ml_secondary_purchases
| 字段名 | 类型 | 说明 |
|--------|------|------|
@@ -60,60 +114,52 @@
| quantity | INT | 购买数量 |
| created_at | TIMESTAMPTZ | 创建时间 |
#### 2.3.3 免单奖励记录表 ml_free_order_rewards
#### 3.3.3 免单奖励记录表 ml_free_order_rewards
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| merchant_id | UUID | 商家ID新增 |
| user_id | UUID | 获得奖励的用户ID |
| share_record_id | UUID | 关联分享记录ID |
| amount | DECIMAL(10,2) | 奖励金额 |
| status | INT | 状态0-待处理1-已发放2-已清零 |
| balance_before | DECIMAL(10,2) | 发放前余额 |
| balance_after | DECIMAL(10,2) | 发放后余额 |
| cleared_at | TIMESTAMPTZ | 清零时间 |
| cleared_by | UUID | 清零操作人 |
| created_at | TIMESTAMPTZ | 创建时间 |
### 2.4 API 接口
### 3.4 前端逻辑变更
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/share/create | POST | 创建分享记录,生成分享码 |
| /api/share/info | GET | 获取分享详情和进度 |
| /api/share/validate | GET | 验证分享码有效性 |
| /api/share/my-records | GET | 获取我的分享记录列表 |
| /api/share/rewards | GET | 获取我的免单奖励记录 |
| /api/admin/clear-balance | POST | 商家清零用户余额(后台) |
#### 3.4.1 订单列表页/订单详情页
### 2.5 前端页面
```typescript
// 判断是否显示分享免单按钮
async function checkShareFreeEnabled(merchantId: string): Promise<boolean> {
const config = await supabaseService.getMerchantPromotionConfig(merchantId)
return config?.promotion_enabled && config?.share_free_enabled
}
#### 2.5.1 分享弹窗(订单详情页)
- 显示分享链接/二维码
- 显示当前进度X/4
- 分享按钮(微信好友、朋友圈)
// 在订单卡片中
<view v-if="order.status >= 2 && order.status <= 4 && isShareFreeEnabled(order.merchant_id)"
class="share-free-row" @click="shareForFree(order)">
<text>🎁 </text>
</view>
```
#### 2.5.2 我的分享
- 分享记录列表
- 每条记录显示:商品信息、进度、状态
- 邀请好友按钮
#### 3.4.2 我的余额
#### 2.5.3 我的余额页
- 余额显示
- 免单奖励记录
- 提现说明(联系商家微信)
- 需要按商家分组显示余额
- 或显示总余额,点击查看各商家余额明细
---
## 、会员等级系统
## 、会员等级系统
### 3.1 功能描述
### 4.1 功能描述
商家可设置多个会员等级,每个等级对应不同的优惠折扣。用户可通过以下方式升级:
1. 商家手动设置等级
2. 累计消费金额自动升级
会员等级系统为**全局功能**,用户等级在所有商家通用。用户可通过累计消费金额自动升级,或由商家手动设置等级。
### 3.2 等级设置
### 4.2 等级设置(全局配置)
| 等级 | 名称 | 升级条件 | 折扣 |
|------|------|---------|------|
@@ -124,92 +170,33 @@
| 4 | 钻石会员 | 累计消费10000元 | 88折 |
| 5 | VIP会员 | 商家手动设置 | 85折 |
### 3.3 数据库设计
### 4.3 数据库设计
#### 3.3.1 会员等级配置表 ml_member_levels
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | INT | 等级ID |
| name | VARCHAR(50) | 等级名称 |
| min_amount | DECIMAL(10,2) | 升级最低消费金额 |
| discount | DECIMAL(5,4) | 折扣率0.85表示85折 |
| icon | VARCHAR(200) | 等级图标 |
| description | TEXT | 等级说明 |
| sort_order | INT | 排序 |
| status | INT | 状态0-禁用1-启用 |
#### 3.3.2 用户会员信息扩展ml_user_profiles 扩展字段)
| 字段名 | 类型 | 说明 |
|--------|------|------|
| member_level | INT | 当前会员等级 |
| total_spent | DECIMAL(10,2) | 累计消费金额 |
| level_updated_at | TIMESTAMPTZ | 等级更新时间 |
| manual_level | BOOLEAN | 是否手动设置等级 |
| manual_level_by | UUID | 手动设置操作人 |
#### 3.3.3 会员等级变更记录表 ml_member_level_logs
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| user_id | UUID | 用户ID |
| old_level | INT | 原等级 |
| new_level | INT | 新等级 |
| reason | VARCHAR(200) | 变更原因 |
| operator_id | UUID | 操作人(自动则为空) |
| created_at | TIMESTAMPTZ | 创建时间 |
### 3.4 API 接口
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/member/levels | GET | 获取会员等级列表 |
| /api/member/my-info | GET | 获取我的会员信息 |
| /api/member/upgrade-check | POST | 检查并升级会员等级 |
| /api/admin/set-member-level | POST | 商家手动设置用户等级 |
### 3.5 前端页面
#### 3.5.1 会员中心页
- 当前等级显示
- 等级进度条
- 等级权益说明
- 升级攻略
#### 3.5.2 商品详情页
- 显示会员价
- 显示折扣信息
保持原有设计不变,会员等级为全局配置。
---
## 、经销点返利系统
## 、经销点返利系统
### 4.1 功能描述
### 5.1 功能描述
商家可创建多个经销点,每个经销点有独立的推广码。经销点通过推广码产生的订单可获得返利,返利按单数计算
商家可创建多个经销点,每个经销点有独立的推广码。经销点通过推广码产生的订单可获得返利。
### 4.2 返利规则
**重要变更:**
- 经销点返利为商家级别功能
- 商家需先开启经销点功能才能使用
| 单数范围 | 返利金额/单 |
|---------|------------|
| 1-10单 | 2元/单 |
| 11-50单 | 3元/单 |
| 51-100单 | 4元/单 |
| 100单以上 | 5元/单 |
### 5.2 数据库设计
### 4.3 数据库设计
#### 4.3.1 经销点表 ml_distribution_points
#### 5.2.1 经销点表 ml_distribution_points
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| merchant_id | UUID | 商家ID新增 |
| name | VARCHAR(100) | 经销点名称 |
| contact_name | VARCHAR(50) | 联系人 |
| contact_phone | VARCHAR(20) | 联系电话 |
| address | VARCHAR(200) | 地址 |
| invite_code | VARCHAR(20) | 邀请码(唯一) |
| owner_id | UUID | 负责人用户ID |
| status | INT | 状态0-禁用1-启用 |
@@ -218,156 +205,92 @@
| balance | DECIMAL(10,2) | 可提现余额 |
| created_at | TIMESTAMPTZ | 创建时间 |
#### 4.3.2 经销点订单关联表 ml_distribution_orders
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| distribution_id | UUID | 经销点ID |
| order_id | UUID | 订单ID |
| user_id | UUID | 下单用户ID |
| order_amount | DECIMAL(10,2) | 订单金额 |
| rebate_amount | DECIMAL(10,2) | 返利金额 |
| status | INT | 状态0-待结算1-已结算 |
| settled_at | TIMESTAMPTZ | 结算时间 |
| created_at | TIMESTAMPTZ | 创建时间 |
#### 4.3.3 返利配置表 ml_rebate_config
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| min_orders | INT | 最小单数 |
| max_orders | INT | 最大单数 |
| rebate_per_order | DECIMAL(10,2) | 每单返利金额 |
| status | INT | 状态 |
#### 4.3.4 返利提现记录表 ml_rebate_withdrawals
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| distribution_id | UUID | 经销点ID |
| amount | DECIMAL(10,2) | 提现金额 |
| status | INT | 状态0-待处理1-已完成2-已拒绝 |
| handled_by | UUID | 处理人 |
| handled_at | TIMESTAMPTZ | 处理时间 |
| created_at | TIMESTAMPTZ | 创建时间 |
### 4.4 API 接口
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/distribution/info | GET | 获取经销点信息 |
| /api/distribution/orders | GET | 获取经销点订单列表 |
| /api/distribution/rebate-summary | GET | 获取返利统计 |
| /api/distribution/withdraw | POST | 申请提现 |
| /api/admin/distributions | GET | 获取经销点列表(后台) |
| /api/admin/create-distribution | POST | 创建经销点(后台) |
| /api/admin/settle-rebate | POST | 结算返利(后台) |
### 4.5 前端页面
#### 4.5.1 经销点中心页(经销点负责人)
- 经销点信息
- 今日/本月订单数
- 累计返利
- 可提现余额
- 申请提现按钮
#### 4.5.2 经销点订单页
- 订单列表
- 筛选(日期、状态)
- 每单返利金额显示
---
## 五、用户余额系统
## 六、余额变动记录表
### 5.1 数据库设计
#### 5.1.1 用户余额表 ml_user_balance
### 6.1 ml_balance_records按商家区分
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| merchant_id | UUID | 商家ID新增 |
| user_id | UUID | 用户ID |
| balance | DECIMAL(10,2) | 当前余额 |
| frozen_balance | DECIMAL(10,2) | 冻结余额 |
| total_earned | DECIMAL(10,2) | 累计获得 |
| total_withdrawn | DECIMAL(10,2) | 累计提现 |
| updated_at | TIMESTAMPTZ | 更新时间 |
#### 5.1.2 余额变动记录表 ml_balance_records
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | UUID | 主键 |
| user_id | UUID | 用户ID |
| type | VARCHAR(50) | 类型free_order-免单rebate-返利withdraw-提现clear-清零 |
| amount | DECIMAL(10,2) | 变动金额(正数增加,负数减少) |
| type | VARCHAR(50) | 类型 |
| amount | DECIMAL(10,2) | 变动金额 |
| balance_before | DECIMAL(10,2) | 变动前余额 |
| balance_after | DECIMAL(10,2) | 变动后余额 |
| related_id | UUID | 关联ID |
| description | VARCHAR(200) | 描述 |
| operator_id | UUID | 操作人(系统操作为空) |
| operator_id | UUID | 操作人 |
| created_at | TIMESTAMPTZ | 创建时间 |
---
## 六、前端页面汇总
## 七、API 接口变更
### 6.1 消费者端新增页面
### 7.1 新增接口
| 页面 | 路径 | 说明 |
| 接口 | 方法 | 说明 |
|------|------|------|
| 我的分享 | /pages/mall/consumer/share/my-shares | 分享记录列表 |
| 分享详情 | /pages/mall/consumer/share/detail | 分享进度详情 |
| 我的余额 | /pages/mall/consumer/balance/index | 余额和奖励记录 |
| 会员中心 | /pages/mall/consumer/member/index | 会员等级信息 |
| 会员权益 | /pages/mall/consumer/member/benefits | 等级权益说明 |
| /api/merchant/promotion-config | GET | 获取商家推销配置 |
| /api/merchant/user-balance | GET | 获取用户在某商家的余额 |
| /api/merchant/user-balance-list | GET | 获取用户所有商家余额列表 |
### 6.2 经销点端页面(可选独立入口)
### 7.2 修改接口
| 页面 | 路径 | 说明 |
|------|------|------|
| 经销点中心 | /pages/distribution/index | 经销点首页 |
| 经销点订单 | /pages/distribution/orders | 订单列表 |
| 返利记录 | /pages/distribution/rebates | 返利记录 |
| 提现申请 | /pages/distribution/withdraw | 提现页面 |
| 接口 | 变更说明 |
|------|---------|
| /api/share/create | 新增merchant_id参数 |
| /api/share/my-records | 按merchant_id筛选 |
| /api/balance/info | 新增merchant_id参数 |
---
## 七、开发优先级
## 八、前端页面变更
### 第一阶段(核心功能)
1. 用户余额系统
2. 分享免单系统
3. 会员等级系统
### 8.1 消费者端
### 第二阶段(扩展功能)
1. 经销点返利系统
2. 后台管理功能
3. 数据统计报表
| 页面 | 变更说明 |
|------|---------|
| 订单列表/详情 | 根据商家配置显示分享免单按钮 |
| 我的余额 | 按商家分组显示余额,或显示余额列表 |
| 我的分享 | 按商家筛选分享记录 |
### 8.2 商家端(新增)
| 页面 | 说明 |
|------|------|
| 推销模式设置 | 开启/关闭分享免单、经销点功能 |
| 用户余额管理 | 查看用户余额、清零操作 |
| 经销点管理 | 创建、编辑经销点 |
---
## 八、注意事项
## 九、开发优先级
1. **安全性**
- 分享码唯一且不可预测
- 防止刷单作弊(同一用户多次购买不计入
- 余额变动需有完整记录
### 第一阶段
1. 商家推销配置表SQL
2. 修改用户余额表结构(按商家区分
3. 修改分享记录表结构添加merchant_id
4. 前端:根据商家配置显示分享免单按钮
2. **性能**
- 高频查询使用缓存
- 大数据量分页处理
### 第二阶段
1. 商家端推销模式设置页面
2. 用户余额按商家分组显示
3. 经销点返利系统
3. **合规性**
- 返利模式需符合当地法规
- 用户协议需明确说明规则
---
4. **扩展性**
- 返利规则可配置
- 会员等级可扩展
- 支持多种分享渠道
## 十、SQL变更清单
需要执行以下SQL变更
1. **新增商家推销配置表** `ml_merchant_promotion_config`
2. **修改用户余额表** 添加merchant_id字段改为联合唯一约束
3. **修改分享记录表** 添加merchant_id字段
4. **修改余额记录表** 添加merchant_id字段
5. **修改免单奖励表** 添加merchant_id字段
6. **修改经销点表** 添加merchant_id字段
详细SQL语句见`promotion_system_tables_v2.sql`

View File

@@ -43,7 +43,7 @@
</view>
<view class="level-detail">
<text class="level-title">{{ level.name }}</text>
<text class="level-condition">{{ level.description || ('累计消费' + level.min_amount + '元') }}</text>
<text class="level-condition">{{ level.description != null && level.description != '' ? level.description : ('累计消费' + level.min_amount + '元') }}</text>
</view>
</view>
<view class="level-right">
@@ -166,15 +166,18 @@ const loadMemberInfo = async (): Promise<void> => {
const nextLevelRaw = result.get('next_level')
if (nextLevelRaw != null) {
const nextLevelAny = nextLevelRaw as any
if (typeof nextLevelAny._getValue === 'function') {
memberInfo.value.next_level = {
id: (nextLevelAny._getValue('id') as number) ?? 0,
name: (nextLevelAny._getValue('name') as string) ?? '',
min_amount: (nextLevelAny._getValue('min_amount') as number) ?? 0,
discount: 1.0,
description: null
}
let nextLevelObj: UTSJSONObject | null = null
if (nextLevelRaw instanceof UTSJSONObject) {
nextLevelObj = nextLevelRaw
} else {
nextLevelObj = JSON.parse(JSON.stringify(nextLevelRaw)) as UTSJSONObject
}
memberInfo.value.next_level = {
id: nextLevelObj.getNumber('id') ?? 0,
name: nextLevelObj.getString('name') ?? '',
min_amount: nextLevelObj.getNumber('min_amount') ?? 0,
discount: 1.0,
description: null
}
}
} catch (e) {
@@ -189,17 +192,20 @@ const loadLevels = async (): Promise<void> => {
for (let i = 0; i < result.length; i++) {
const item = result[i]
const itemAny = item as any
if (typeof itemAny._getValue === 'function') {
parsed.push({
id: (itemAny._getValue('id') as number) ?? 0,
name: (itemAny._getValue('name') as string) ?? '',
min_amount: (itemAny._getValue('min_amount') as number) ?? 0,
discount: (itemAny._getValue('discount') as number) ?? 1.0,
description: itemAny._getValue('description') as string | null
})
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
parsed.push({
id: itemObj.getNumber('id') ?? 0,
name: itemObj.getString('name') ?? '',
min_amount: itemObj.getNumber('min_amount') ?? 0,
discount: itemObj.getNumber('discount') ?? 1.0,
description: itemObj.getString('description')
})
}
levels.value = parsed
@@ -216,17 +222,20 @@ const loadLogs = async (): Promise<void> => {
for (let i = 0; i < result.length; i++) {
const item = result[i]
const itemAny = item as any
if (typeof itemAny._getValue === 'function') {
parsed.push({
id: (itemAny._getValue('id') as string) ?? '',
old_level: (itemAny._getValue('old_level') as number) ?? 0,
new_level: (itemAny._getValue('new_level') as number) ?? 0,
reason: itemAny._getValue('reason') as string | null,
created_at: (itemAny._getValue('created_at') as string) ?? ''
})
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
parsed.push({
id: itemObj.getString('id') ?? '',
old_level: itemObj.getNumber('old_level') ?? 0,
new_level: itemObj.getNumber('new_level') ?? 0,
reason: itemObj.getString('reason'),
created_at: itemObj.getString('created_at') ?? ''
})
}
logs.value = parsed

View File

@@ -0,0 +1,265 @@
<template>
<scroll-view class="message-detail-page" scroll-y>
<view class="message-header">
<text class="message-title">{{ message.title }}</text>
<text class="message-time">{{ formatTime(message.created_at) }}</text>
</view>
<view class="message-content">
<text class="content-text">{{ message.content }}</text>
</view>
<view v-if="message.link_url" class="message-action" @click="goToLink">
<text class="action-text">查看详情</text>
<text class="action-arrow"></text>
</view>
<view v-if="message.icon_url" class="message-image">
<image :src="message.icon_url" mode="widthFix" class="icon-image" />
</view>
<view v-if="extraInfo.length > 0" class="extra-info">
<view v-for="(item, index) in extraInfo" :key="index" class="extra-item">
<text class="extra-label">{{ item.label }}</text>
<text class="extra-value">{{ item.value }}</text>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type MessageType = {
id: string
type: string
title: string
content: string
icon_url: string | null
link_url: string | null
extra_data: any | null
created_at: string
}
type ExtraInfoItem = {
label: string
value: string
}
const message = ref<MessageType>({
id: '',
type: '',
title: '',
content: '',
icon_url: null,
link_url: null,
extra_data: null,
created_at: ''
})
const extraInfo = ref<ExtraInfoItem[]>([])
const loadMessage = async (id: string) => {
try {
const notifications = await supabaseService.getUserNotifications(null)
const found = notifications.find(n => n.id === id)
if (found != null) {
message.value = {
id: found.id,
type: found.type,
title: found.title,
content: found.content,
icon_url: found.icon_url,
link_url: found.link_url,
extra_data: found.extra_data,
created_at: found.created_at
}
// 解析extra_data
if (found.extra_data != null) {
parseExtraData(found.extra_data)
}
}
} catch (e) {
console.error('加载消息失败:', e)
}
}
const parseExtraData = (data: any) => {
extraInfo.value = []
if (data == null) return
try {
let dataObj: any = data
if (typeof data === 'string') {
dataObj = JSON.parse(data)
}
if (typeof dataObj === 'object') {
const keys = Object.keys(dataObj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = dataObj[key]
if (value != null) {
extraInfo.value.push({
label: formatLabel(key),
value: String(value)
})
}
}
}
} catch (e) {
console.error('解析extra_data失败:', e)
}
}
const formatLabel = (key: string): string => {
const labelMap: Record<string, string> = {
'share_code': '分享码',
'product_name': '商品名称',
'reward_amount': '奖励金额',
'order_no': '订单号',
'buyer_name': '购买者',
'quantity': '数量'
}
return labelMap[key] ?? key
}
const formatTime = (timeStr: string): string => {
if (timeStr == null || timeStr === '') return ''
const date = new Date(timeStr)
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
const hh = date.getHours().toString().padStart(2, '0')
const mm = date.getMinutes().toString().padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}`
}
const goToLink = () => {
const url = message.value.link_url
if (url != null && url !== '') {
if (url.startsWith('/pages/')) {
uni.navigateTo({ url: url })
} else {
uni.setClipboardData({
data: url,
success: () => {
uni.showToast({ title: '链接已复制', icon: 'success' })
}
})
}
}
}
onMounted(() => {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
const options = (currentPage as any).options
if (options != null && options.id != null) {
loadMessage(options.id as string)
}
}
})
</script>
<style>
.message-detail-page {
flex: 1;
height: 100%;
background-color: #f5f5f5;
}
.message-header {
background-color: white;
padding: 20px 16px;
margin-bottom: 8px;
}
.message-title {
font-size: 18px;
font-weight: bold;
color: #333;
display: flex;
margin-bottom: 10px;
}
.message-time {
font-size: 13px;
color: #999;
}
.message-content {
background-color: white;
padding: 16px;
margin-bottom: 8px;
}
.content-text {
font-size: 15px;
color: #333;
line-height: 1.8;
}
.message-action {
background-color: white;
padding: 16px;
margin-bottom: 8px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.action-text {
font-size: 15px;
color: #ff6b35;
}
.action-arrow {
font-size: 18px;
color: #ccc;
}
.message-image {
background-color: white;
padding: 16px;
margin-bottom: 8px;
}
.icon-image {
width: 100%;
border-radius: 8px;
}
.extra-info {
background-color: white;
padding: 16px;
}
.extra-item {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f5f5f5;
}
.extra-item:last-child {
border-bottom: none;
}
.extra-label {
font-size: 14px;
color: #666;
}
.extra-value {
font-size: 14px;
color: #333;
}
</style>

View File

@@ -22,7 +22,7 @@
<view class="product-info" @click="goToProduct(review.product_id)">
<image
class="product-image"
:src="review.product_image || defaultImage"
:src="review.product_image.length > 0 ? review.product_image : defaultImage"
mode="aspectFill"
/>
<view class="product-detail">
@@ -84,7 +84,7 @@
<view class="product-info">
<image
class="product-image"
:src="item.product_image || defaultImage"
:src="item.product_image.length > 0 ? item.product_image : defaultImage"
mode="aspectFill"
/>
<view class="product-detail">

View File

@@ -746,21 +746,16 @@ const shareForFree = async () => {
// 使用 onBackPress 拦截物理返回键和系统导航栏返回
onBackPress((_): boolean => {
const pages = getCurrentPages()
console.log('[order-detail onBackPress] pages count:', pages.length)
if (pages.length > 1) {
// @ts-ignore
const prevPage = pages[pages.length - 2]
// @ts-ignore
const prevRoute: string = prevPage.route ?? ''
if (prevRoute.includes('product-detail') || prevRoute.includes('payment')) {
uni.redirectTo({ url: '/pages/mall/consumer/orders' })
return true
}
} else {
uni.redirectTo({ url: '/pages/mall/consumer/orders' })
return true
// 正常返回上一页
return false
}
return false
// 如果只有当前页面,跳转到 orders
uni.redirectTo({ url: '/pages/mall/consumer/orders' })
return true
})
// 生命周期 - 在所有函数定义之后

View File

@@ -1,4 +1,4 @@
<!-- pages/mall/consumer/orders.uvue -->
<!-- pages/mall/consumer/orders.uvue -->
<template>
<view class="orders-page">
<!-- 顶部标题栏 -->
@@ -88,12 +88,12 @@
v-for="product in order.products"
:key="product.id"
class="order-product"
@click="navigateToProduct(product)"
>
<image
class="product-image"
:src="product.image"
mode="aspectFill"
@click.stop="navigateToProduct(product)"
/>
<view class="product-info">
<view class="product-top-info">
@@ -117,6 +117,14 @@
</view>
</view>
<!-- 分享免单入口 (已付款订单显示: 待发货、待收货、已完成,且商家开启了分享免单) -->
<view v-if="order.status >= 2 && order.status <= 4 && merchantShareFreeEnabled[order.merchant_id]" class="share-free-row" @click.stop="shareForFree(order)">
<text class="share-free-icon">🎁</text>
<text class="share-free-text">分享免单</text>
<text class="share-free-tip">分享给好友,{{ merchantRequiredCount[order.merchant_id] != null ? merchantRequiredCount[order.merchant_id] : 4 }}人购买即可免单</text>
<text class="share-free-arrow"></text>
</view>
<!-- 订单操作 -->
<view class="order-actions" @click.stop="">
<view v-if="order.status === 1" class="action-buttons">
@@ -226,6 +234,10 @@ const activeTab = ref<string>('all')
const statusBarHeight = ref<number>(0)
const searchKeyword = ref<string>('')
// 商家推销配置缓存
const merchantShareFreeEnabled = ref<Record<string, boolean>>({})
const merchantRequiredCount = ref<Record<string, number>>({})
// 订单标签页 - 使用 ref 以便整体替换
const orderTabs = ref<OrderTabItem[]>([
{ id: 'all', name: '全部', count: 0 },
@@ -496,6 +508,9 @@ const loadOrders = async () => {
// Apply current tab filter
filterOrdersByTab()
// 加载商家推销配置
loadMerchantPromotionConfigs(mappedOrders)
} catch (err) {
console.error('加载订单异常:', err)
uni.showToast({ title: '加载订单失败', icon: 'none' })
@@ -532,6 +547,39 @@ onShow(() => {
loadOrders()
})
// 加载商家推销配置
const loadMerchantPromotionConfigs = async (orderList: OrderItem[]) => {
// 收集所有唯一的商家ID
const merchantIds = new Set<string>()
for (let i = 0; i < orderList.length; i++) {
const merchantId = orderList[i].merchant_id
if (merchantId != null && merchantId !== '' && !merchantShareFreeEnabled.value.hasOwnProperty(merchantId)) {
merchantIds.add(merchantId)
}
}
// 批量加载商家配置
const merchantIdArray = Array.from(merchantIds)
for (let i = 0; i < merchantIdArray.length; i++) {
const merchantId = merchantIdArray[i]
try {
const config = await supabaseService.getMerchantPromotionConfig(merchantId)
const promotionEnabled = config.get('promotion_enabled')
const shareFreeEnabled = config.get('share_free_enabled')
const requiredCount = config.get('required_count')
merchantShareFreeEnabled.value[merchantId] =
(promotionEnabled === true || promotionEnabled === 'true') &&
(shareFreeEnabled === true || shareFreeEnabled === 'true')
merchantRequiredCount.value[merchantId] = (requiredCount as number) ?? 4
} catch (e) {
console.error('加载商家推销配置失败:', merchantId, e)
merchantShareFreeEnabled.value[merchantId] = false
merchantRequiredCount.value[merchantId] = 4
}
}
}
const formatDate = (isoString: string): string => {
if (isoString == '') return ''
const date = new Date(isoString)
@@ -799,16 +847,22 @@ const doConfirmReceipt = async (orderId: string) => {
icon: 'success'
})
// 更新本地状态
const index = orders.value.findIndex((o: OrderItem): boolean => o.id === orderId)
if (index !== -1) {
orders.value[index].status = 4
orders.value = [...orders.value]
// 更新 allOrdersList 中的订单状态
const allIndex = allOrdersList.value.findIndex((o: OrderItem): boolean => o.id === orderId)
if (allIndex !== -1) {
allOrdersList.value[allIndex].status = 4
allOrdersList.value = [...allOrdersList.value]
}
// 更新标签计数
updateTabsCounts(allOrdersList.value)
// 重新应用当前标签筛选
filterOrdersByTab()
// 跳转到评价页面
setTimeout(() => {
const order = orders.value.find((o: OrderItem): boolean => o.id === orderId)
const order = allOrdersList.value.find((o: OrderItem): boolean => o.id === orderId)
if (order != null) {
goReview(order)
}
@@ -1030,6 +1084,53 @@ const navigateToProduct = (product: OrderProduct) => {
const goShopping = () => {
uni.switchTab({ url: '/pages/main/index' })
}
const shareForFree = async (order: OrderItem) => {
if (order.products.length === 0) {
uni.showToast({ title: '没有可分享的商品', icon: 'none' })
return
}
const firstProduct = order.products[0]
try {
uni.showLoading({ title: '创建分享...' })
const result = await supabaseService.createShareRecord(
firstProduct.id,
order.id,
'',
firstProduct.name,
firstProduct.image,
firstProduct.price
)
uni.hideLoading()
const shareIdRaw = result.get('id')
const shareCodeRaw = result.get('share_code')
if (shareIdRaw != null && shareCodeRaw != null) {
const shareId = shareIdRaw as string
const shareCode = shareCodeRaw as string
uni.showModal({
title: '分享成功',
content: `您的分享码: ${shareCode}\n分享给好友当有4人购买后即可免单`,
confirmText: '查看详情',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: `/pages/mall/consumer/share/detail?id=${shareId}` })
}
}
})
} else {
uni.showToast({ title: '分享创建失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('[shareForFree] 创建分享失败:', e)
uni.showToast({ title: '分享失败', icon: 'none' })
}
}
</script>
<style>
@@ -1081,7 +1182,7 @@ const goShopping = () => {
.search-input {
flex: 1;
height: 36px;
line-height: normal;
line-height: 36px;
border: 1px solid #dddddd;
border-radius: 18px;
padding: 0 40px 0 16px;
@@ -1677,6 +1778,41 @@ const goShopping = () => {
color: #333;
}
/* 分享免单入口样式 */
.share-free-row {
margin: 0 15px;
padding: 12px 15px;
background: linear-gradient(135deg, #fff5f0 0%, #ffecd2 100%);
border-radius: 8px;
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.share-free-icon {
font-size: 20px;
margin-right: 8px;
}
.share-free-text {
font-size: 14px;
font-weight: bold;
color: #ff6b35;
margin-right: 8px;
}
.share-free-tip {
flex: 1;
font-size: 12px;
color: #ff8c42;
}
.share-free-arrow {
font-size: 16px;
color: #ff8c42;
}
@media screen and (max-width: 320px) {
.tab-item {
padding: 0 10px;

View File

@@ -173,7 +173,7 @@ const loadPaymentMethods = () => {
// 加载用户余额(必须在 onMounted 之前定义)
const loadUserBalance = async () => {
try {
const balance = await supabaseService.getUserBalance()
const balance = await supabaseService.getUserBalanceNumber()
userBalance.value = balance
} catch (err) {
console.error('加载用户余额异常:', err)

View File

@@ -1,5 +1,5 @@
<template>
<scroll-view class="records-page" scroll-y>
<scroll-view class="records-page" direction="vertical">
<view class="empty-state" v-if="!loading && records.length === 0">
<text class="empty-text">暂无兑换记录</text>
</view>
@@ -85,39 +85,33 @@ const loadRecords = async (): Promise<void> => {
let product_image: string | null = null
let product_type = 'coupon'
// 使用 _getValue 方法
if (typeof recordData._getValue === 'function') {
id = (recordData._getValue('id') as string) ?? ''
quantity = (recordData._getValue('quantity') as number) ?? 1
points_used = (recordData._getValue('points_used') as number) ?? 0
status = (recordData._getValue('status') as number) ?? 0
tracking_no = recordData._getValue('tracking_no') as string | null
created_at = (recordData._getValue('created_at') as string) ?? ''
// 获取关联的商品信息
const product = recordData._getValue('product')
if (product != null) {
const productAny = product as any
if (typeof productAny._getValue === 'function') {
product_name = (productAny._getValue('name') as string) ?? ''
product_image = productAny._getValue('image_url') as string | null
product_type = (productAny._getValue('product_type') as string) ?? 'coupon'
}
}
// 转换为 UTSJSONObject
let recordObj: UTSJSONObject | null = null
if (recordData instanceof UTSJSONObject) {
recordObj = recordData
} else {
id = recordData['id'] ?? ''
quantity = recordData['quantity'] ?? 1
points_used = recordData['points_used'] ?? 0
status = recordData['status'] ?? 0
tracking_no = recordData['tracking_no'] ?? null
created_at = recordData['created_at'] ?? ''
const product = recordData['product']
if (product != null) {
product_name = product['name'] ?? ''
product_image = product['image_url'] ?? null
product_type = product['product_type'] ?? 'coupon'
recordObj = JSON.parse(JSON.stringify(recordData)) as UTSJSONObject
}
id = recordObj.getString('id') ?? ''
quantity = recordObj.getNumber('quantity') ?? 1
points_used = recordObj.getNumber('points_used') ?? 0
status = recordObj.getNumber('status') ?? 0
tracking_no = recordObj.getString('tracking_no')
created_at = recordObj.getString('created_at') ?? ''
// 获取关联的商品信息
const product = recordObj.get('product')
if (product != null) {
let productObj: UTSJSONObject | null = null
if (product instanceof UTSJSONObject) {
productObj = product
} else {
productObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
}
product_name = productObj.getString('name') ?? ''
product_image = productObj.getString('image_url')
product_type = productObj.getString('product_type') ?? 'coupon'
}
parsed.push({

View File

@@ -1,5 +1,5 @@
<template>
<scroll-view class="exchange-page" scroll-y>
<scroll-view class="exchange-page" direction="vertical">
<view class="header">
<view class="points-info">
<text class="points-label">可用积分</text>
@@ -50,7 +50,7 @@
>
<image
class="product-image"
:src="product.image_url || defaultImage"
:src="product.image_url != null && product.image_url.length > 0 ? product.image_url : defaultImage"
mode="aspectFill"
/>
<view class="product-info">
@@ -86,7 +86,7 @@
<view class="popup-product" v-if="selectedProduct != null">
<image
class="popup-product-image"
:src="selectedProduct.image_url || defaultImage"
:src="selectedProduct.image_url != null && selectedProduct.image_url.length > 0 ? selectedProduct.image_url : defaultImage"
mode="aspectFill"
/>
<view class="popup-product-info">
@@ -200,7 +200,6 @@ const loadProducts = async (): Promise<void> => {
const parsed: PointProduct[] = []
for (let i = 0; i < productList.length; i++) {
const item = productList[i]
const itemAny = item as any
let id = ''
let name = ''
@@ -212,29 +211,23 @@ const loadProducts = async (): Promise<void> => {
let stock = 0
let status = 1
// UTSJSONObject2 需要使用 _getValue 方法
if (typeof itemAny._getValue === 'function') {
id = (itemAny._getValue('id') as string) ?? ''
name = (itemAny._getValue('name') as string) ?? ''
description = itemAny._getValue('description') as string | null
image_url = itemAny._getValue('image_url') as string | null
product_type = (itemAny._getValue('product_type') as string) ?? 'coupon'
points_required = (itemAny._getValue('points_required') as number) ?? 0
original_price = itemAny._getValue('original_price') as number | null
stock = (itemAny._getValue('stock') as number) ?? 0
status = (itemAny._getValue('status') as number) ?? 1
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
id = itemAny['id'] ?? ''
name = itemAny['name'] ?? ''
description = itemAny['description'] ?? null
image_url = itemAny['image_url'] ?? null
product_type = itemAny['product_type'] ?? 'coupon'
points_required = itemAny['points_required'] ?? 0
original_price = itemAny['original_price'] ?? null
stock = itemAny['stock'] ?? 0
status = itemAny['status'] ?? 1
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
id = itemObj.getString('id') ?? ''
name = itemObj.getString('name') ?? ''
description = itemObj.getString('description')
image_url = itemObj.getString('image_url')
product_type = itemObj.getString('product_type') ?? 'coupon'
points_required = itemObj.getNumber('points_required') ?? 0
original_price = itemObj.getNumber('original_price')
stock = itemObj.getNumber('stock') ?? 0
status = itemObj.getNumber('status') ?? 1
const product: PointProduct = {
id,
name,
@@ -411,7 +404,7 @@ onMounted(() => {
}
.product-card {
width: calc(50% - 8px);
width: 48%;
margin: 4px;
background-color: white;
border-radius: 8px;
@@ -453,7 +446,7 @@ onMounted(() => {
.product-points {
display: flex;
flex-direction: row;
align-items: baseline;
align-items: center;
}
.points-num {
@@ -476,7 +469,6 @@ onMounted(() => {
.product-original {
font-size: 12px;
color: #999;
text-decoration: line-through;
}
.empty-state {
@@ -574,7 +566,7 @@ onMounted(() => {
.popup-product-points {
display: flex;
flex-direction: row;
align-items: baseline;
align-items: center;
margin-top: 8px;
}
@@ -701,7 +693,7 @@ onMounted(() => {
width: 60px;
height: 60px;
background-color: #52c41a;
border-radius: 50%;
border-radius: 30px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -1,5 +1,5 @@
<template>
<scroll-view class="points-page" scroll-y>
<scroll-view class="points-page" direction="vertical">
<view class="points-header">
<view class="points-info">
<text class="points-label">当前积分</text>
@@ -439,7 +439,7 @@ const formatDate = (dateStr: string): string => {
width: 32px;
height: 32px;
background-color: #52c41a;
border-radius: 50%;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
@@ -571,7 +571,7 @@ const formatDate = (dateStr: string): string => {
background-color: white;
border-radius: 16px 16px 0 0;
width: 100%;
max-height: 60%;
max-height: 400px;
padding: 16px;
}

View File

@@ -373,7 +373,7 @@ onMounted(() => {
.day-cell {
width: 14.28%;
aspect-ratio: 1;
height: 45px;
display: flex;
flex-direction: column;
align-items: center;
@@ -406,7 +406,7 @@ onMounted(() => {
width: 16px;
height: 16px;
background-color: #ff6b35;
border-radius: 50%;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -78,7 +78,7 @@
<view class="review-header">
<image
class="user-avatar"
:src="review.user_avatar || defaultAvatar"
:src="review.user_avatar.length > 0 ? review.user_avatar : defaultAvatar"
mode="aspectFill"
/>
<view class="user-info">
@@ -109,7 +109,7 @@
@click="previewImage(review.images, idx)"
/>
<view class="more-images" v-if="review.images.length > 3">
<text>+{{ review.images.length - 3 }}</text>
<text class="more-images-text">+{{ review.images.length - 3 }}</text>
</view>
</view>
@@ -131,7 +131,7 @@
@click="toggleLike(review)"
>
<text class="like-icon">{{ review.is_liked ? '❤' : '♡' }}</text>
<text class="like-count">{{ review.like_count || 0 }}</text>
<text class="like-count">{{ review.like_count != null ? review.like_count : 0 }}</text>
</view>
</view>
</view>
@@ -620,7 +620,7 @@ onMounted(() => {
justify-content: center;
}
.more-images text {
.more-images-text {
font-size: 14px;
color: white;
}

View File

@@ -554,8 +554,10 @@ const goBack = (): void => {
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
position: sticky;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
@@ -654,7 +656,7 @@ const goBack = (): void => {
.rating-label {
font-size: 15px;
color: #333333;
font-weight: 500;
font-weight: bold;
margin-right: 20px;
}
@@ -810,7 +812,7 @@ const goBack = (): void => {
.section-title {
font-size: 16px;
font-weight: 600;
font-weight: bold;
color: #333333;
margin-bottom: 20px;
padding-left: 10px;

View File

@@ -1,7 +1,7 @@
<template>
<scroll-view class="share-detail-page" scroll-y>
<scroll-view class="share-detail-page" direction="vertical">
<view class="product-section">
<image class="product-image" :src="shareRecord.product_image || defaultImage" mode="aspectFill" />
<image class="product-image" :src="shareRecord.product_image != null && shareRecord.product_image.length > 0 ? shareRecord.product_image : defaultImage" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ shareRecord.product_name }}</text>
<text class="product-price">¥{{ shareRecord.product_price }}</text>
@@ -144,21 +144,25 @@ const loadShareDetail = async (): Promise<void> => {
const recordRaw = result.get('share_record')
if (recordRaw != null) {
const recordAny = recordRaw as any
if (typeof recordAny._getValue === 'function') {
shareRecord.value = {
id: (recordAny._getValue('id') as string) ?? '',
product_name: (recordAny._getValue('product_name') as string) ?? '',
product_image: recordAny._getValue('product_image') as string | null,
product_price: (recordAny._getValue('product_price') as number) ?? 0,
share_code: (recordAny._getValue('share_code') as string) ?? '',
required_count: (recordAny._getValue('required_count') as number) ?? 4,
current_count: (recordAny._getValue('current_count') as number) ?? 0,
status: (recordAny._getValue('status') as number) ?? 0,
reward_amount: recordAny._getValue('reward_amount') as number | null,
created_at: (recordAny._getValue('created_at') as string) ?? '',
completed_at: recordAny._getValue('completed_at') as string | null
}
let recordObj: UTSJSONObject | null = null
if (recordRaw instanceof UTSJSONObject) {
recordObj = recordRaw
} else {
recordObj = JSON.parse(JSON.stringify(recordRaw)) as UTSJSONObject
}
shareRecord.value = {
id: recordObj.getString('id') ?? '',
product_name: recordObj.getString('product_name') ?? '',
product_image: recordObj.getString('product_image'),
product_price: recordObj.getNumber('product_price') ?? 0,
share_code: recordObj.getString('share_code') ?? '',
required_count: recordObj.getNumber('required_count') ?? 4,
current_count: recordObj.getNumber('current_count') ?? 0,
status: recordObj.getNumber('status') ?? 0,
reward_amount: recordObj.getNumber('reward_amount'),
created_at: recordObj.getString('created_at') ?? '',
completed_at: recordObj.getString('completed_at')
}
}
@@ -169,17 +173,20 @@ const loadShareDetail = async (): Promise<void> => {
for (let i = 0; i < arr.length; i++) {
const item = arr[i]
const itemAny = item as any
if (typeof itemAny._getValue === 'function') {
parsed.push({
id: (itemAny._getValue('id') as string) ?? '',
buyer_id: (itemAny._getValue('buyer_id') as string) ?? '',
buyer_name: '用户' + (i + 1),
quantity: (itemAny._getValue('quantity') as number) ?? 1,
created_at: (itemAny._getValue('created_at') as string) ?? ''
})
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
parsed.push({
id: itemObj.getString('id') ?? '',
buyer_id: itemObj.getString('buyer_id') ?? '',
buyer_name: '用户' + (i + 1),
quantity: itemObj.getNumber('quantity') ?? 1,
created_at: itemObj.getString('created_at') ?? ''
})
}
buyers.value = parsed
@@ -368,7 +375,7 @@ onMounted(() => {
.progress-numbers {
display: flex;
flex-direction: row;
align-items: baseline;
align-items: center;
margin-left: 12px;
}

View File

@@ -1,5 +1,5 @@
<template>
<scroll-view class="share-page" scroll-y>
<scroll-view class="share-page" direction="vertical">
<view class="share-summary">
<view class="summary-item">
<text class="summary-value">{{ totalShares }}</text>
@@ -46,7 +46,7 @@
<view v-else class="share-list">
<view class="share-item" v-for="share in shares" :key="share.id" @click="goToShareDetail(share.id)">
<image class="product-image" :src="share.product_image || defaultImage" mode="aspectFill" />
<image class="product-image" :src="share.product_image != null && share.product_image.length > 0 ? share.product_image : defaultImage" mode="aspectFill" />
<view class="share-info">
<text class="product-name">{{ share.product_name }}</text>
<view class="progress-section">
@@ -118,23 +118,26 @@ const loadShares = async (): Promise<void> => {
for (let i = 0; i < result.length; i++) {
const item = result[i]
const itemAny = item as any
if (typeof itemAny._getValue === 'function') {
parsed.push({
id: (itemAny._getValue('id') as string) ?? '',
product_id: (itemAny._getValue('product_id') as string) ?? '',
product_name: (itemAny._getValue('product_name') as string) ?? '',
product_image: itemAny._getValue('product_image') as string | null,
product_price: (itemAny._getValue('product_price') as number) ?? 0,
share_code: (itemAny._getValue('share_code') as string) ?? '',
required_count: (itemAny._getValue('required_count') as number) ?? 4,
current_count: (itemAny._getValue('current_count') as number) ?? 0,
status: (itemAny._getValue('status') as number) ?? 0,
reward_amount: itemAny._getValue('reward_amount') as number | null,
created_at: (itemAny._getValue('created_at') as string) ?? ''
})
let itemObj: UTSJSONObject | null = null
if (item instanceof UTSJSONObject) {
itemObj = item
} else {
itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
}
parsed.push({
id: itemObj.getString('id') ?? '',
product_id: itemObj.getString('product_id') ?? '',
product_name: itemObj.getString('product_name') ?? '',
product_image: itemObj.getString('product_image'),
product_price: itemObj.getNumber('product_price') ?? 0,
share_code: itemObj.getString('share_code') ?? '',
required_count: itemObj.getNumber('required_count') ?? 4,
current_count: itemObj.getNumber('current_count') ?? 0,
status: itemObj.getNumber('status') ?? 0,
reward_amount: itemObj.getNumber('reward_amount'),
created_at: itemObj.getString('created_at') ?? ''
})
}
shares.value = parsed

View File

@@ -7,7 +7,7 @@
<view v-else-if="plan == null" class="empty">未找到该方案</view>
<view v-else class="card">
<text class="name">{{ plan['name'] }}</text>
<text class="desc">{{ plan['description'] || '—' }}</text>
<text class="desc">{{ plan['description'] != null && (plan['description'] as string).length > 0 ? plan['description'] : '—' }}</text>
<view class="price-row">
<text class="price">¥{{ plan['price'] }}</text>
@@ -45,10 +45,11 @@ const toFeatureArray = (features: any): Array<string> => {
const arr: Array<string> = []
if (features == null) return arr
if (features instanceof UTSJSONObject) {
const keys = Object.keys(features as any)
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
const v = (features as UTSJSONObject)[k]
const featureMap = (features as UTSJSONObject).toMap()
const entries = featureMap.entries()
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
const v = entry.value
const vs = typeof v === 'string' ? v : JSON.stringify(v)
arr.push(vs)
}

View File

@@ -10,7 +10,7 @@
<text class="plan-name">{{ p['name'] }}</text>
<text v-if="p['billing_period'] === 'yearly'" class="badge">年付优惠</text>
</view>
<text class="plan-desc">{{ p['description'] || '适用于大部分使用场景' }}</text>
<text class="plan-desc">{{ p['description'] != null && (p['description'] as string).length > 0 ? p['description'] : '适用于大部分使用场景' }}</text>
<view class="price-row">
<text class="price">¥{{ p['price'] }}</text>
<text class="period">/{{ p['billing_period'] === 'yearly' ? '年' : '月' }}</text>
@@ -43,10 +43,11 @@ const toFeatureArray = (features: any): Array<string> => {
const arr: Array<string> = []
if (features == null) return arr
if (features instanceof UTSJSONObject) {
const keys = Object.keys(features as any)
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
const v = (features as UTSJSONObject)[k]
const featureMap = (features as UTSJSONObject).toMap()
const entries = featureMap.entries()
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
const v = entry.value
const vs = typeof v === 'string' ? v : JSON.stringify(v)
arr.push(vs)
}

View File

@@ -234,7 +234,7 @@ const resetTransactions = (): void => {
// 加载余额信息
const loadBalance = async (): Promise<void> => {
try {
const realBalance = await supabaseService.getUserBalance()
const realBalance = await supabaseService.getUserBalanceNumber()
balance.value = realBalance
const statsData: StatsType = {

View File

@@ -99,7 +99,7 @@ const isValid = computed((): boolean => {
const loadData = async (): Promise<void> => {
try {
const bal = await supabaseService.getUserBalance()
const bal = await supabaseService.getUserBalanceNumber()
balance.value = bal
const res = await supabaseService.getUserBankCards()