consumer模块完成度95%,检查消费者前端bug并修复
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -182,7 +182,6 @@ const selectAddress = (item: Address) => {
|
||||
<style>
|
||||
.address-list-page {
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 @@
|
||||
- 返回的是 result,resultdata 一般可以 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. 可空类型属性在模板中使用时需要处理 null:profile.gender ?? 'other'
|
||||
@@ -289,7 +333,7 @@
|
||||
105. 使用辅助函数 safeGetString、safeGetNumber 处理数据库字段
|
||||
|
||||
================================================================================
|
||||
十二、构造函数限制
|
||||
十三、构造函数限制
|
||||
================================================================================
|
||||
|
||||
1. 不支持 Number() 构造函数,使用 as number 类型转换
|
||||
@@ -297,7 +341,7 @@
|
||||
3. 示例:Number(x) → x as number,String(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] 不支持
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
265
pages/mall/consumer/message-detail.uvue
Normal file
265
pages/mall/consumer/message-detail.uvue
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
// 生命周期 - 在所有函数定义之后
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user