4564 lines
162 KiB
Markdown
4564 lines
162 KiB
Markdown
# UTS Android 兼容性开发规范指南
|
||
|
||
> 以下为 uni-app x (UTS) Android 端开发常见注意事项与踩坑点,建议所有开发成员遵循:
|
||
|
||
================================================================================
|
||
一、基础语法规范
|
||
================================================================================
|
||
|
||
1. 变量声明
|
||
- 只能使用 let 和 const,不能使用 var
|
||
- 变量声明必须有显式类型或初始化值
|
||
- 不支持 undefined 类型,变量未赋值就是 null
|
||
- 不支持 undefined 关键字,判断是否存在要用 != null
|
||
|
||
2. 类型定义
|
||
- 只适合用 type,不适合使用 interface(interface 在 kotlin/swift 中另有不同)
|
||
- 不支持 Intersection Type(交叉类型)
|
||
- 不支持 Index Signature(索引签名)
|
||
- 类型推断严格,必要时用 as Type 明确类型
|
||
- 不支持内联对象类型(Object Literal Type),需要单独定义 type
|
||
|
||
3. 函数定义
|
||
- 函数必须在使用前定义(不支持函数提升)
|
||
- 在 setup 模式下,调用的函数必须在调用之前定义
|
||
- 依赖关系需要明确:被调用的函数必须先定义
|
||
- 这与 JavaScript 的函数提升行为不同,UTS 更接近 C/Java 的编译方式
|
||
|
||
4. 循环
|
||
- for 循环的 i 必须写明类型:let i: number = 0
|
||
- 不要用 forEach、map,数组遍历用 for 循环
|
||
- 嵌套的数组方法调用可能导致类型推断失败,应改用 for 循环
|
||
|
||
================================================================================
|
||
二、类型与对象访问
|
||
================================================================================
|
||
|
||
1. any 类型访问
|
||
- 不能直接访问 any 类型对象的属性
|
||
- 需要将对象转换为 UTSJSONObject 类型后使用 getString()、getNumber() 等方法访问属性
|
||
- any 类型属性访问需转换为 Record<string, any> 后用索引访问
|
||
- 使用索引访问属性时,推荐使用方括号语法 obj['property'] 而非点语法 obj.property
|
||
- any 类型不支持索引访问 obj['key'],必须先转换为 UTSJSONObject
|
||
|
||
2. UTSJSONObject 使用
|
||
- 用 utils/utis 下的 UTSJSONObject 做类型转换
|
||
- 不要用 safeget,只要 UTSJSONObject 就好
|
||
- 需要创建动态对象时,应使用 new UTSJSONObject() 然后调用 .set() 方法
|
||
- 对于 type 定义的对象类型,同样需要使用 UTSJSONObject
|
||
- 使用 getString()、getNumber() 方法获取属性值
|
||
|
||
3. 数组类型
|
||
- 数组类型建议写成 Array<Type>,不要用 Type[] 简写
|
||
- 空数组需要明确指定类型,如 [] as string[]
|
||
- 数组元素需要明确的类型定义才能在模板中正确访问属性
|
||
- 对于 any[] 或 reactive 数组,访问元素属性时需要先转换为 Record<string, any> 或 any[]
|
||
|
||
4. 对象操作
|
||
- 不支持 Object.keys()、Object.values()、Object.entries()
|
||
- 不支持 Record<K, V> 对象字面量语法
|
||
- 对象字面量 {...} 只能用于构造类型(class),不能用于接口(interface)
|
||
- reactive 对象在 UTS 中不支持索引器赋值操作
|
||
|
||
================================================================================
|
||
三、条件判断与逻辑运算
|
||
================================================================================
|
||
|
||
1. if 条件
|
||
- if 判断只能接 boolean 类型,不能是其他类型的值
|
||
- 判断空要用 !== null,不能用 !变量(uts android 不支持 !在变量前面的判断空方式)
|
||
- 模板中的 || 运算符左边必须是 boolean 类型
|
||
- 可空类型使用可选链 ?. 和空值合并 ??
|
||
- 字符串判断空要用:variable != null && variable !== ''
|
||
|
||
2. 逻辑运算符
|
||
- || 表示逻辑或
|
||
- && 表示逻辑与
|
||
- ! 表示逻辑非(但 !变量 不支持用于判断空)
|
||
- ?? 表示空值合并运算符(当左侧为 null 时返回右侧值)
|
||
- ts 的为空则使用默认值的语法在 uts 中不能用 ||,要用 ?? 来代替
|
||
|
||
================================================================================
|
||
四、组件与模板
|
||
================================================================================
|
||
|
||
1. 表单与输入
|
||
- 表单优先用 form 组件
|
||
- 不支持 uni-easyinput,用 input 代替
|
||
- 时间选择用 uni_modules/lime-date-time-picker
|
||
|
||
2. 选择器
|
||
- uts android 不支持 picker,用 picker-view 或 uni.showActionSheet
|
||
- 一维的优先用 uni.showActionSheet
|
||
- picker-view 的事件用 UniPickerViewChangeEvent
|
||
|
||
3. 导航与布局
|
||
- 不支持 uni-nav-bar,先删除
|
||
- 不支持 uni-data-select,用 picker-view 代替
|
||
- 不支持 uni-datetime-picker,用 components/picker-date 或 components/picker-time 代替
|
||
- 不支持 uni-icons
|
||
|
||
4. 模板注意事项
|
||
- 跟 template 交互的变量尽量用一维变量(不要嵌套对象)
|
||
- 模板中可空类型必须使用 ?. 安全访问
|
||
- 模板中访问可空类型属性前必须先判空 v-if="order != null"
|
||
|
||
================================================================================
|
||
五、CSS 样式限制
|
||
================================================================================
|
||
|
||
1. 布局方式
|
||
- 只支持 display: flex
|
||
- 不支持 display: grid
|
||
- 不支持 gap
|
||
- 不支持 table、grid、grid-template-columns
|
||
|
||
2. 单位与计算
|
||
- 不支持 calc()
|
||
- 不支持的单位: vh
|
||
- property value `100%` is not supported for min-height (supported values are: number|pixel)
|
||
- property value `calc(33.33% - 10px)` is not supported for min-width
|
||
|
||
3. 选择器
|
||
- [APP-ANDROID] 不支持伪类选择器
|
||
- [APP-IOS] 不支持伪类选择器
|
||
- ERROR: Selector `.login-button[disabled]` is not supported. uvue only support classname selector
|
||
|
||
4. 其他样式
|
||
- 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 使用
|
||
================================================================================
|
||
|
||
- scroll-view 在 uni-app-x 中不是用 scroll-y=true
|
||
- 而是要用 direction="vertical"
|
||
|
||
================================================================================
|
||
七、异步与回调
|
||
================================================================================
|
||
|
||
- uni.showModal 的 success 回调不能是 async 函数
|
||
- 解决方案:创建独立的 async 函数,在回调中调用
|
||
- UTS 中箭头函数 () => {} 有时会导致 "Parenthesized expression cannot be empty" 错误
|
||
- 解决方案:使用普通函数 function name(): Type {} 代替箭头函数
|
||
|
||
================================================================================
|
||
八、响应式数据
|
||
================================================================================
|
||
|
||
- 对于需要整体替换的数组,推荐使用 ref 而非 reactive
|
||
- 使用 ref 时,通过 .value 进行整体替换可以正确触发响应式更新
|
||
- ref 数组元素不能直接整体替换,需要修改元素属性
|
||
- 对于可能为 null 的参数,需要显式检查后再传递给函数
|
||
|
||
================================================================================
|
||
九、类型导入
|
||
================================================================================
|
||
|
||
- 类型导入需要使用 type 关键字
|
||
- 一般情况下,尽可能用强类型模式
|
||
- uni_modules 的情况下,尽量把 type 定义到 interface 里面
|
||
- 数据获取争夺都用强类型方式,查询或 rpc 查询用 supa.from.executeAs<T>() 方式
|
||
- 返回的是 result,resultdata 一般可以 as Array<T>
|
||
|
||
================================================================================
|
||
十、常见错误速查
|
||
================================================================================
|
||
|
||
1. "Unresolved reference" - 函数未定义或顺序错误
|
||
2. "Cannot create an instance of an abstract class" - Record<K,V> 不能实例化
|
||
3. "Assignment type mismatch" - 类型不匹配,需要显式类型转换
|
||
4. "if condition must be boolean" - if 条件必须是 boolean 类型
|
||
5. "Index Signature is not supported" - 不支持索引签名
|
||
6. "Intersection Type is not supported" - 不支持交叉类型
|
||
7. "Parenthesized expression cannot be empty" - 箭头函数问题,改用普通函数
|
||
8. "找不到名称" - 属性访问问题,需转换为 UTSJSONObject
|
||
9. "参数类型不匹配" - 参数类型错误,需要显式类型转换
|
||
|
||
================================================================================
|
||
十一、简明速记(100+条)
|
||
================================================================================
|
||
|
||
1. 表单优先用 form 组件
|
||
2. 跟 template 交互的变量尽量用一维变量
|
||
3. 不要用 forEach、map、safeget,数组遍历用 for 循环,类型转换用 UTSJSONObject
|
||
4. 数组类型建议写成 Array<Type>,不要用 Type[] 简写
|
||
5. 不支持 undefined,变量未赋值就是 null,判断用 != null
|
||
6. 变量声明只能用 let 或 const,不能用 var
|
||
7. 判断空要用 !== null,不能用 !变量
|
||
8. 只支持 type,不建议用 interface
|
||
9. for 循环的 i 必须写明类型:let i: number = 0
|
||
10. 逻辑或用 ||,空值合并用 ??,不能混用
|
||
11. if 判断只能是 boolean 类型
|
||
12. 不支持索引签名(Index Signature)
|
||
13. 类型推断严格,必要时用 as Type 明确类型
|
||
14. 不支持 Intersection Type
|
||
15. picker 用 picker-view 或 uni.showActionSheet 替代
|
||
16. 样式只支持 display: flex,不支持 gap、grid、calc()、伪类选择器
|
||
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' : ''"
|
||
94. 使用外部类型定义时,确保所有属性都有默认值,避免 null 导致类型不匹配
|
||
95. Supabase insert/update 在 .uvue 文件中直接调用可能报类型错误,建议封装到 .uts 服务文件中调用
|
||
96. 可空类型属性在模板中使用时需要处理 null:profile.gender ?? 'other'
|
||
97. 可空数字类型比较前需要先检查 null:profile.height_cm != null && profile.height_cm > 0
|
||
98. CSS 伪类选择器 :last-child、:first-child、:nth-child() 不支持,需要移除
|
||
99. Supabase 批量 insert 数组参数类型不匹配,需要改为循环逐条 insert
|
||
100. CSS 中独立的属性(没有选择器)会导致编译错误:Return type mismatch: expected 'Map<String, Map<String, Map<String, Any>>>'
|
||
101. Supabase 查询整数字段用字符串形式:.eq('status', '1') 而非 .eq('status', 1)
|
||
102. Supabase insert 整数字段也用字符串:target_type: '1' 而非 target_type: 1
|
||
103. 数据库返回的字段类型可能不符,需要用 typeof 检查后安全转换
|
||
104. UUID 字段可能返回 Integer,需要安全转换:typeof val === 'number' ? val.toString() : val
|
||
105. 使用辅助函数 safeGetString、safeGetNumber 处理数据库字段
|
||
|
||
================================================================================
|
||
十二、构造函数限制
|
||
================================================================================
|
||
|
||
1. 不支持 Number() 构造函数,使用 as number 类型转换
|
||
2. 不支持 String() 构造函数,使用 as string 类型转换
|
||
3. 示例:Number(x) → x as number,String(i) → i as string
|
||
|
||
================================================================================
|
||
十三、取反操作符限制
|
||
================================================================================
|
||
|
||
1. 不支持 !变量 的取反操作符用于判断空
|
||
2. 字符串判断空:variable == null || variable === ''
|
||
3. 示例:!this.selectedSkuId → (this.selectedSkuId == null || this.selectedSkuId === '')
|
||
|
||
================================================================================
|
||
十四、parseInt/parseFloat 限制
|
||
================================================================================
|
||
|
||
1. parseInt() 参数必须是 string 类型
|
||
2. 如果变量是 number 类型,直接使用,不要调用 parseInt
|
||
3. 示例:parseInt(this.quantity) 错误,quantity 是 number,直接用 this.quantity
|
||
|
||
================================================================================
|
||
十五、Object.keys() 不支持
|
||
================================================================================
|
||
|
||
1. 不支持 Object.keys() 方法
|
||
2. 替代方案:使用 JSON.stringify() 或 for 循环遍历
|
||
3. UTSJSONObject 可用 .keys() 方法(但某些版本可能不支持)
|
||
|
||
================================================================================
|
||
十六、typeof 函数检查不支持
|
||
================================================================================
|
||
|
||
1. 不支持 typeof xxx === 'function' 语法
|
||
2. 替代方案:使用 try-catch 包裹方法调用
|
||
|
||
================================================================================
|
||
十七、as unknown as 语法不支持
|
||
================================================================================
|
||
|
||
1. 不支持 as unknown as 双重类型转换
|
||
2. 直接使用 as 目标类型:obj as UTSJSONObject
|
||
|
||
================================================================================
|
||
十八、可空类型方法返回值
|
||
================================================================================
|
||
|
||
1. decodeURIComponent() 返回 String?,需要处理 null
|
||
2. charCodeAt() 返回 Number?,需要处理 null
|
||
3. 示例:const code = str.charCodeAt(i); if (code != null) { ... }
|
||
|
||
================================================================================
|
||
十九、内联对象类型不支持
|
||
================================================================================
|
||
|
||
1. 不支持 Array<{id: string, name: string}> 这种内联类型定义
|
||
2. 需要在 types 文件中单独定义 type
|
||
3. 示例:定义 type ItemType = { id: string, name: string },然后使用 Array<ItemType>
|
||
|
||
================================================================================
|
||
二十、eventChannel 不支持
|
||
================================================================================
|
||
|
||
1. uni.navigateTo 的 success 回调中 res.eventChannel 不支持
|
||
2. 替代方案:使用 Storage 或全局变量传递数据
|
||
|
||
================================================================================
|
||
二十一、链式调用问题
|
||
================================================================================
|
||
|
||
1. .map().join() 链式调用可能导致类型推断失败
|
||
2. 替代方案:使用 for 循环或分步处理
|
||
|
||
================================================================================
|
||
二十二、v-model 类型限制
|
||
================================================================================
|
||
|
||
1. input 的 v-model 期望 string 类型
|
||
2. 如果变量是 number,使用 :value="variable.toString()" 替代 v-model
|
||
|
||
================================================================================
|
||
二十三、setup 模式函数定义顺序(重要)
|
||
================================================================================
|
||
|
||
在 <script setup lang="uts"> 中,函数定义顺序至关重要:
|
||
|
||
1. 基本原则
|
||
- 函数必须在调用之前定义(不支持 JavaScript 的函数提升)
|
||
- 这与 JavaScript 的行为不同,UTS 更接近 C/Java 的编译方式
|
||
|
||
2. watch 和 onMounted 中的函数调用
|
||
- watch() 和 onMounted() 是立即执行的
|
||
- 它们内部调用的函数必须在 watch/onMounted 之前定义
|
||
- 错误示例:
|
||
```typescript
|
||
// 错误:resetData 和 loadRefunds 还没定义
|
||
watch(activeTab, () => {
|
||
resetData()
|
||
loadRefunds()
|
||
})
|
||
|
||
const resetData = () => { ... }
|
||
const loadRefunds = async () => { ... }
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 正确!先定义函数
|
||
const resetData = () => { ... }
|
||
const loadRefunds = async () => { ... }
|
||
|
||
// 再调用 watch/onMounted
|
||
watch(activeTab, () => {
|
||
resetData()
|
||
loadRefunds()
|
||
})
|
||
onMounted(() => {
|
||
loadRefunds()
|
||
})
|
||
```
|
||
|
||
3. 推荐的代码组织顺序:
|
||
```typescript
|
||
<script setup lang="uts">
|
||
// 1. 导入语句
|
||
import { ref, watch, onMounted } from 'vue'
|
||
import { supabaseService } from '@/utils/supabaseService'
|
||
|
||
// 2. 类型定义
|
||
type MyType = { id: string, name: string }
|
||
|
||
// 3. 响应式变量声明
|
||
const data = ref<Array<MyType>>([])
|
||
const loading = ref<boolean>(false)
|
||
|
||
// 4. 工具函数(不依赖其他函数的)
|
||
const getUserId = (): string => { ... }
|
||
|
||
// 5. 业务函数(可能依赖工具函数)
|
||
const loadData = async () => { ... }
|
||
const resetData = () => { ... }
|
||
|
||
// 6. 事件处理函数
|
||
const handleClick = () => { ... }
|
||
|
||
// 7. 生命周期钩子和 watch(放在最后)
|
||
watch(data, () => { ... })
|
||
onMounted(() => { ... })
|
||
</script>
|
||
```
|
||
|
||
4. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx"
|
||
- 原因:函数在调用之后定义
|
||
- 解决:调整函数定义顺序,确保被调用的函数先定义
|
||
|
||
================================================================================
|
||
二十四、箭头函数限制(重要)
|
||
================================================================================
|
||
|
||
1. 箭头函数不支持默认参数值
|
||
- 错误示例:
|
||
```typescript
|
||
const loadData = async (page: number = 1) => { ... } // 错误!
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 方案1:不使用默认参数,调用时显式传参
|
||
const loadData = async (page: number): Promise<void> => { ... }
|
||
loadData(1) // 调用时传参
|
||
|
||
// 方案2:使用普通函数定义
|
||
function loadData(page: number = 1): Promise<void> { ... }
|
||
```
|
||
|
||
2. 箭头函数需要明确返回类型
|
||
- 建议显式声明返回类型:`: Promise<void>` 或 `: string` 等
|
||
- 示例:`const getData = async (): Promise<string> => { ... }`
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"Anonymous functions cannot specify default values for their parameters"
|
||
- 原因:箭头函数使用了默认参数
|
||
- 解决:移除默认参数,改用显式传参或普通函数定义
|
||
|
||
================================================================================
|
||
二十五、数组元素属性访问(重要)
|
||
================================================================================
|
||
|
||
1. Array<any> 元素属性访问问题
|
||
- 数组元素类型为 any 时,不能直接访问属性
|
||
- 错误示例:
|
||
```typescript
|
||
const steps: Array<any> = [...]
|
||
steps[1].time = 'xxx' // 错误!找不到名称 "time"
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const steps: Array<any> = [...]
|
||
const step = steps[1] as UTSJSONObject
|
||
step.set('time', 'xxx')
|
||
```
|
||
|
||
2. 推荐方案:定义明确的类型
|
||
```typescript
|
||
type StepType = { status: number, title: string, time: string, desc: string }
|
||
const steps: Array<StepType> = [...]
|
||
steps[1].time = 'xxx' // 正确!
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx"
|
||
- 原因:any 类型数组元素无法直接访问属性
|
||
- 解决:转换为 UTSJSONObject 或定义明确类型
|
||
|
||
================================================================================
|
||
二十六、类型定义与可空类型(重要)
|
||
================================================================================
|
||
|
||
1. 类型定义中可空类型的处理
|
||
- 如果属性可能为 null,必须显式声明为可空类型
|
||
- 错误示例:
|
||
```typescript
|
||
type ItemType = {
|
||
sku_specifications: any // 错误!如果值可能为 null
|
||
}
|
||
const spec = getSpec() // 返回 Any?
|
||
item.sku_specifications = spec // 类型不匹配
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
type ItemType = {
|
||
sku_specifications: any | null // 正确!
|
||
}
|
||
const specRaw = getSpec()
|
||
const spec = (specRaw != null) ? (specRaw as any) : null
|
||
item.sku_specifications = spec
|
||
```
|
||
|
||
2. UTSJSONObject.get() 返回值处理
|
||
- get() 方法返回 Any? 类型
|
||
- 必须先判断 null,再进行类型转换
|
||
- 示例:
|
||
```typescript
|
||
const raw = obj.get('field')
|
||
const value = (raw != null) ? (raw as string) : null
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"参数类型不匹配:实际类型为 'Any?',预期类型为 'Any'"
|
||
- 原因:可空类型不能直接赋值给非空类型
|
||
- 解决:修改类型定义为可空类型,或处理 null 情况
|
||
|
||
================================================================================
|
||
二十七、模板中的可空类型处理(重要)
|
||
================================================================================
|
||
|
||
1. 模板中可选链限制
|
||
- 模板中 ?.length 等可选链操作可能报错
|
||
- 错误示例:
|
||
```html
|
||
<view v-if="refund.status_history?.length > 0">
|
||
```
|
||
- 正确示例:
|
||
```html
|
||
<view v-if="refund.status_history != null && refund.status_history.length > 0">
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"Operator call is prohibited on a nullable receiver of type 'Number?'"
|
||
- 原因:可空类型不能直接调用操作符
|
||
- 解决:显式判断 null 后再访问属性
|
||
|
||
================================================================================
|
||
二十八、解构赋值限制(重要)
|
||
================================================================================
|
||
|
||
1. UTS 中解构赋值可能有问题
|
||
- 错误示例:
|
||
```typescript
|
||
const { data, error } = await supa.from('table').select('*')
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const response = await supa.from('table').select('*').execute()
|
||
const data = response.data
|
||
const error = response.error
|
||
```
|
||
|
||
2. 推荐使用 .execute() 获取完整响应
|
||
- 使用 response.data 和 response.error 访问结果
|
||
- 避免使用解构赋值
|
||
|
||
================================================================================
|
||
二十九、API 响应数据处理(重要)
|
||
================================================================================
|
||
|
||
1. response.data 返回 Any? 类型
|
||
- 错误示例:
|
||
```typescript
|
||
order.value = orderRes.data // 错误,Any? 不能赋值给 Any
|
||
const itemsArray = itemsRes.data ?? [] // 错误!无法推断空数组类型
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
if (orderRes.data != null) {
|
||
order.value = orderRes.data as UTSJSONObject
|
||
}
|
||
|
||
const rawData = itemsRes.data
|
||
let itemsArray: Array<any> = []
|
||
if (rawData != null) {
|
||
itemsArray = rawData as Array<any>
|
||
}
|
||
```
|
||
|
||
2. 空数组类型推断问题
|
||
- `[] ?? []` 或 `data ?? []` 无法推断类型
|
||
- 必须显式声明数组类型
|
||
- 示例:
|
||
```typescript
|
||
// 错误
|
||
const arr = data ?? []
|
||
|
||
// 正确
|
||
let arr: Array<any> = []
|
||
if (data != null) {
|
||
arr = data as Array<any>
|
||
}
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"Assignment type mismatch: actual type is 'Any?', but 'Any' was expected"
|
||
- 原因:可空类型 Any? 不能直接赋值给非空类型
|
||
- 解决:先判断 null,再进行类型转换
|
||
|
||
- 错误信息:"Cannot infer type for this parameter" 或 "Not enough information to infer type"
|
||
- 原因:空数组无法推断类型
|
||
- 解决:显式声明数组类型
|
||
|
||
================================================================================
|
||
三十、ref 对象字面量类型(重要)
|
||
================================================================================
|
||
|
||
1. ref 对象字面量必须定义类型
|
||
- 错误示例:
|
||
```typescript
|
||
const merchantRating = ref({
|
||
description: 5,
|
||
logistics: 5,
|
||
service: 5
|
||
})
|
||
merchantRating.value.description = rating // 错误!找不到名称 "description"
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
type MerchantRatingType = {
|
||
description: number
|
||
logistics: number
|
||
service: number
|
||
}
|
||
|
||
const merchantRating = ref<MerchantRatingType>({
|
||
description: 5,
|
||
logistics: 5,
|
||
service: 5
|
||
} as MerchantRatingType)
|
||
merchantRating.value.description = rating // 正确!
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx"
|
||
- 原因:ref 对象字面量没有显式类型定义
|
||
- 解决:定义 type 并在 ref 中指定泛型类型
|
||
|
||
================================================================================
|
||
三十一、if 条件与可空类型(重要)
|
||
================================================================================
|
||
|
||
1. if 条件必须是 boolean 类型
|
||
- 错误示例:
|
||
```typescript
|
||
if (merchant.value) { ... } // 错误,MerchantType? 不是 boolean
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
if (merchant.value != null) { ... } // 正确!
|
||
```
|
||
|
||
2. 可空类型不能直接用于 if 条件
|
||
- 必须使用 != null 或 !== null 判断
|
||
- 示例:
|
||
```typescript
|
||
// 错误
|
||
if (obj) { ... }
|
||
|
||
// 正确
|
||
if (obj != null) { ... }
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"Condition type mismatch: inferred type is 'XXX?' but 'Boolean' was expected"
|
||
- 原因:可空类型不能直接用于 if 条件
|
||
- 解决:使用 != null 判断
|
||
|
||
================================================================================
|
||
三十二、throw 语句限制(重要)
|
||
================================================================================
|
||
|
||
1. throw 语句不能抛出 Any 类型
|
||
- 错误示例:
|
||
```typescript
|
||
const { error } = await supa.from('table').insert(data)
|
||
if (error !== null) {
|
||
throw error // 错误,Any 类型不能抛出
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const res = await supa.from('table').insert(data).execute()
|
||
if (res.error != null) {
|
||
console.error('操作失败:', res.error)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
return // 处理错误而非抛出
|
||
}
|
||
```
|
||
|
||
2. 推荐错误处理方式
|
||
- 记录错误日志
|
||
- 显示用户提示
|
||
- 返回或终止操作
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"类型不匹配 推断类型是 'Any',但预期的是'Throwable'"
|
||
- 原因:UTS 中 throw 只能抛出 Throwable 类型
|
||
- 解决:处理错误而非抛出,或创建 Error 对象
|
||
|
||
================================================================================
|
||
三十三、模板中的可选链与属性访问(重要)
|
||
================================================================================
|
||
|
||
1. 模板中可选链限制
|
||
- 错误示例:
|
||
```html
|
||
<text>{{ order?.order_no }}</text>
|
||
<text>{{ formatTime(order?.created_at) }}</text>
|
||
```
|
||
- 正确示例:
|
||
```html
|
||
<text>{{ order != null ? order.order_no : '' }}</text>
|
||
<text>{{ formatTime(order != null ? order.created_at : '') }}</text>
|
||
```
|
||
|
||
2. ref<any> 在模板中无法访问属性
|
||
- 错误示例:
|
||
```typescript
|
||
const order = ref<any>({})
|
||
```
|
||
```html
|
||
<text>{{ order?.order_no }}</text> <!-- 错误!找不到名称 -->
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
type OrderType = {
|
||
id: string
|
||
order_no: string
|
||
created_at: string
|
||
}
|
||
const order = ref<OrderType | null>(null)
|
||
```
|
||
```html
|
||
<text>{{ order != null ? order.order_no : '' }}</text>
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx"
|
||
- 原因:any 类型或可选链在模板中无法正确推断属性
|
||
- 解决:定义明确类型,使用三元表达式代替可选链
|
||
|
||
================================================================================
|
||
三十四、数组操作限制(重要)
|
||
================================================================================
|
||
|
||
1. 展开运算符 [...arr] 不支持
|
||
- 错误示例:
|
||
```typescript
|
||
const shuffled = [...allGuessItems.value]
|
||
searchResults.value.push(...newItems)
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 复制数组
|
||
const arr: Array<any> = []
|
||
for (let i: number = 0; i < allGuessItems.value.length; i++) {
|
||
arr.push(allGuessItems.value[i])
|
||
}
|
||
// 追加元素
|
||
for (let i: number = 0; i < newItems.length; i++) {
|
||
searchResults.value.push(newItems[i])
|
||
}
|
||
```
|
||
|
||
2. Array.from(new Set()) 不支持
|
||
- 错误示例:
|
||
```typescript
|
||
const uniqueNames = Array.from(new Set(names))
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 手动去重
|
||
const uniqueNames: Array<string> = []
|
||
for (let i: number = 0; i < names.length; i++) {
|
||
let found = false
|
||
for (let j: number = 0; j < uniqueNames.length; j++) {
|
||
if (uniqueNames[j] === names[i]) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if (found === false) {
|
||
uniqueNames.push(names[i])
|
||
}
|
||
}
|
||
```
|
||
|
||
3. Map.entries() 和 Map.keys() 不支持,需使用forEach遍历
|
||
- 错误示例:
|
||
```typescript
|
||
const keywordCount = new Map<string, number>()
|
||
// ... 填充数据 ...
|
||
|
||
// 错误:entries() 不能作为函数调用
|
||
const sortedKeywords = Array.from(keywordCount.entries())
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, limit)
|
||
.map(entry => entry[0])
|
||
|
||
// 错误:keys() 也不能作为函数调用
|
||
const keySet = keywordCount.keys()
|
||
for (let i = 0; i < keySet.length; i++) { ... }
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const keywordCount = new Map<string, number>()
|
||
// ... 填充数据 ...
|
||
|
||
// 定义类型用于存储键值对
|
||
type KeywordEntry = {
|
||
keyword: string
|
||
count: number
|
||
}
|
||
|
||
// 使用forEach遍历Map(UTS支持)
|
||
const entryArray: KeywordEntry[] = []
|
||
keywordCount.forEach((value: number, key: string) => {
|
||
entryArray.push({
|
||
keyword: key,
|
||
count: value
|
||
})
|
||
})
|
||
|
||
// 排序时需要明确类型注解
|
||
entryArray.sort((a: KeywordEntry, b: KeywordEntry): number => {
|
||
return b.count - a.count
|
||
})
|
||
|
||
// 提取结果
|
||
const sortedKeywords: string[] = []
|
||
const maxCount = Math.min(entryArray.length, limit)
|
||
for (let i: number = 0; i < maxCount; i++) {
|
||
sortedKeywords.push(entryArray[i].keyword)
|
||
}
|
||
```
|
||
|
||
4. Supabase upsert 方法不支持,需使用 insert
|
||
- 错误示例:
|
||
```typescript
|
||
await supa
|
||
.from('ml_browse_history')
|
||
.upsert(browseRecord)
|
||
.execute()
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 使用 insert 替代 upsert
|
||
await supa
|
||
.from('ml_browse_history')
|
||
.insert(browseRecord)
|
||
.execute()
|
||
```
|
||
|
||
5. .sort() 带回调函数需要明确类型注解
|
||
- 错误示例:
|
||
```typescript
|
||
const shuffled = arr.sort(() => Math.random() - 0.5)
|
||
```
|
||
- 正确示例(自定义排序需要类型注解):
|
||
```typescript
|
||
// Fisher-Yates 洗牌算法(推荐用于随机排序)
|
||
for (let i: number = arr.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1))
|
||
const temp = arr[i]
|
||
arr[i] = arr[j]
|
||
arr[j] = temp
|
||
}
|
||
```
|
||
- 或者使用带类型注解的sort回调:
|
||
```typescript
|
||
type SortableItem = {
|
||
id: string
|
||
count: number
|
||
}
|
||
|
||
const items: SortableItem[] = [...]
|
||
|
||
// 必须明确参数类型和返回类型
|
||
items.sort((a: SortableItem, b: SortableItem): number => {
|
||
return b.count - a.count
|
||
})
|
||
```
|
||
|
||
================================================================================
|
||
三十五、异步操作限制(重要)
|
||
================================================================================
|
||
|
||
1. Promise.all 可能有问题
|
||
- 错误示例:
|
||
```typescript
|
||
const [prodResp, shopResp] = await Promise.all([
|
||
supabaseService.searchProducts(keyword, page, limit),
|
||
supabaseService.searchShops(keyword)
|
||
])
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const prodResp = await supabaseService.searchProducts(keyword, page, limit)
|
||
const shopResp = await supabaseService.searchShops(keyword)
|
||
```
|
||
|
||
2. 解构赋值可能有问题
|
||
- 错误示例:
|
||
```typescript
|
||
const [prodResp, shopResp] = await Promise.all([...])
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const prodResp = await func1()
|
||
const shopResp = await func2()
|
||
```
|
||
|
||
================================================================================
|
||
三十六、事件对象处理(重要)
|
||
================================================================================
|
||
|
||
1. 事件对象属性访问
|
||
- 错误示例:
|
||
```typescript
|
||
const onInput = (e: any) => {
|
||
const val = e.detail.value // 错误!找不到名称 "detail"
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const onInput = (e: any) => {
|
||
const eObj = e as UTSJSONObject
|
||
const detailRaw = eObj.get('detail')
|
||
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject())
|
||
const val = detail.getString('value') ?? ''
|
||
}
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx"
|
||
- 原因:any 类型事件对象无法直接访问属性
|
||
- 解决:转换为 UTSJSONObject 后使用 .get() 方法
|
||
|
||
================================================================================
|
||
三十七、数组索引访问限制(重要)
|
||
================================================================================
|
||
|
||
1. 数组索引访问可能越界
|
||
- 错误示例:
|
||
```typescript
|
||
const texts = ['非常差', '差', '一般', '好', '非常好']
|
||
return texts[rating - 1] ?? '未评价' // 可能越界或类型推断失败
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
if (rating === 1) return '非常差'
|
||
if (rating === 2) return '差'
|
||
if (rating === 3) return '一般'
|
||
if (rating === 4) return '好'
|
||
if (rating === 5) return '非常好'
|
||
return '未评价'
|
||
```
|
||
|
||
2. 推荐使用 if-else 或 Map 替代数组索引查找
|
||
|
||
================================================================================
|
||
三十八、函数可选参数限制(重要)
|
||
================================================================================
|
||
|
||
1. 可选参数不能跳过传递
|
||
- UTS Android 不支持跳过可选参数传递
|
||
- 如果函数有多个可选参数,必须按顺序传递所有参数
|
||
- 错误示例:
|
||
```typescript
|
||
// 函数定义
|
||
async addToCart(productId: string, quantity: number = 1, skuId?: string, merchantId?: string): Promise<boolean>
|
||
|
||
// 错误调用 - 跳过了 merchantId 参数
|
||
await supabaseService.addToCart(productId, 1, '')
|
||
// 编译错误:No value passed for parameter 'merchantId'
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 方案1:给可选参数添加默认值
|
||
async addToCart(productId: string, quantity: number = 1, skuId: string = '', merchantId: string = ''): Promise<boolean>
|
||
|
||
// 方案2:调用时传递所有参数
|
||
await supabaseService.addToCart(productId, 1, '', '')
|
||
```
|
||
|
||
2. 可选参数定义规范
|
||
- 推荐使用 `param: Type = defaultValue` 而非 `param?: Type`
|
||
- `param?: Type` 在 Android 端调用时仍需传递参数
|
||
- `param: Type = defaultValue` 可以在不传参时使用默认值
|
||
- 示例:
|
||
```typescript
|
||
// 不推荐 - 调用时仍需传递参数
|
||
function foo(a: string, b?: string, c?: string): void
|
||
|
||
// 推荐 - 可以跳过参数使用默认值
|
||
function foo(a: string, b: string = '', c: string = ''): void
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 错误信息:"No value passed for parameter 'xxx'"
|
||
- 原因:可选参数在 Android 端不能跳过
|
||
- 解决:
|
||
1. 修改函数签名,使用默认值 `param: Type = defaultValue`
|
||
2. 调用时传递所有参数
|
||
|
||
4. 最佳实践
|
||
- 对于有多个可选参数的函数,统一使用默认值语法
|
||
- 调用时显式传递所有参数,避免依赖可选参数跳过
|
||
- 在服务层函数定义中,优先使用 `= ''` 或 `= 0` 等默认值
|
||
|
||
================================================================================
|
||
三十九、模板中的非空断言限制(重要)
|
||
================================================================================
|
||
|
||
1. 模板中不支持非空断言操作符 `!`
|
||
- UTS Android 模板中不能使用 `variable!` 非空断言
|
||
- 错误示例:
|
||
```html
|
||
<text v-if="product.original_price != null && product.original_price! > product.price">
|
||
```
|
||
- 正确示例:
|
||
```html
|
||
<text v-if="product.original_price != null && product.original_price > product.price">
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"参数类型不匹配:实际类型为 'Number?',预期类型为 'Number'"
|
||
- 原因:模板中使用非空断言 `!` 不被支持
|
||
- 解决:移除非空断言 `!`,直接使用变量进行比较
|
||
|
||
3. 最佳实践
|
||
- 在模板中,先用 `!= null` 判断可空类型,然后直接使用变量
|
||
- UTS 编译器会在 `!= null` 判断后自动识别变量为非空类型
|
||
|
||
================================================================================
|
||
四十、未导入类型的处理(重要)
|
||
================================================================================
|
||
|
||
1. 未导入的类型不能直接使用
|
||
- 在页面中使用的类型必须先导入或使用 UTSJSONObject 替代
|
||
- 错误示例:
|
||
```typescript
|
||
// Shop 类型未导入
|
||
const s = shopRespData[i] as Shop
|
||
const id = s.id // 找不到名称 "id"
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 使用 UTSJSONObject
|
||
const s = shopRespData[i] as UTSJSONObject
|
||
const id = s.getString('id') ?? ''
|
||
const name = s.getString('shop_name') ?? ''
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"找不到名称 'XXX'"
|
||
- 原因:类型未导入或类型定义不存在
|
||
- 解决:
|
||
1. 导入需要的类型:`import { Shop } from '@/utils/supabaseService.uts'`
|
||
2. 使用 UTSJSONObject 替代:`as UTSJSONObject` 然后用 `.getString()`、`.getNumber()` 访问属性
|
||
|
||
3. 最佳实践
|
||
- 对于简单的数据转换,推荐使用 UTSJSONObject
|
||
- 避免在多个文件中重复定义相同的类型
|
||
- 如果需要类型安全,从服务层导入类型定义
|
||
|
||
================================================================================
|
||
四十一、服务层数据字段完整性(重要)
|
||
================================================================================
|
||
|
||
1. 服务层返回数据必须包含所有必要字段
|
||
- 从数据库获取数据时,必须正确映射所有需要的字段
|
||
- 错误示例:
|
||
```typescript
|
||
const product: Product = {
|
||
id: prodObj.getString('id') ?? '',
|
||
name: prodObj.getString('name') ?? '',
|
||
// 错误:merchant_id 硬编码为空字符串
|
||
merchant_id: ''
|
||
} as Product
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const product: Product = {
|
||
id: prodObj.getString('id') ?? '',
|
||
name: prodObj.getString('name') ?? '',
|
||
// 正确:从数据库获取 merchant_id
|
||
merchant_id: prodObj.getString('merchant_id') ?? ''
|
||
} as Product
|
||
```
|
||
|
||
2. 调用服务层方法时必须传递完整参数
|
||
- 页面调用服务层方法时,需要传递所有必要参数
|
||
- 错误示例:
|
||
```typescript
|
||
// 错误:merchant_id 传空字符串
|
||
await supabaseService.addToCart(productId, 1, '', '')
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 正确:从商品对象获取 merchant_id
|
||
const merchantId = product.merchant_id ?? ''
|
||
await supabaseService.addToCart(productId, 1, '', merchantId)
|
||
```
|
||
|
||
3. 编译错误提示
|
||
- 问题表现:数据添加到数据库失败,或添加的数据不完整
|
||
- 原因:服务层或页面层缺少必要字段的传递
|
||
- 解决:
|
||
1. 检查服务层数据映射是否完整
|
||
2. 检查页面调用时是否传递了所有必要参数
|
||
|
||
4. 最佳实践
|
||
- 服务层方法返回的对象应包含数据库视图的所有字段
|
||
- 页面调用服务层方法时,应从数据对象中获取并传递所有参数
|
||
- 对于关联数据(如 merchant_id),确保在数据加载时一并获取
|
||
|
||
================================================================================
|
||
四十二、模板中的非运算符限制(重要)
|
||
================================================================================
|
||
|
||
1. 模板中不支持 `!` 非运算符
|
||
- UTS Android 模板中不能使用 `!variable` 非运算符
|
||
- 错误示例:
|
||
```html
|
||
<view v-if="!brand.logo_url">
|
||
```
|
||
- 正确示例:
|
||
```html
|
||
<view v-if="brand.logo_url == null || brand.logo_url == ''">
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"找不到名称 not'"
|
||
- 原因:模板中不支持非运算符 `!`
|
||
- 解决:使用显式的比较表达式替代
|
||
|
||
3. 最佳实践
|
||
- 使用 `== null` 或 `== ''` 检查空值
|
||
- 使用 `!= null && != ''` 检查非空值
|
||
|
||
================================================================================
|
||
四十三、索引访问限制(重要)
|
||
================================================================================
|
||
|
||
1. 不支持 `(obj as any)['key']` 索引访问方式
|
||
- UTS Android 不支持对 any 类型使用索引访问
|
||
- 错误示例:
|
||
```typescript
|
||
const detail = (e as any)['detail']
|
||
val = detail['value'] ?? ''
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 方案1:使用 UTSJSONObject
|
||
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
|
||
const detail = eObj.get('detail') as UTSJSONObject
|
||
val = detail.getString('value') ?? ''
|
||
|
||
// 方案2:先判断类型再转换
|
||
if (e instanceof UTSJSONObject) {
|
||
const eObj = e as UTSJSONObject
|
||
const detail = eObj.get('detail') as UTSJSONObject
|
||
val = detail.getString('value') ?? ''
|
||
}
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch"
|
||
- 原因:any 类型不支持索引访问
|
||
- 解决:转换为 UTSJSONObject 后使用 `.get()` 方法
|
||
|
||
3. 最佳实践
|
||
- 统一使用 UTSJSONObject 处理动态对象
|
||
- 使用 `.get()`、`.getString()`、`.getNumber()` 方法访问属性
|
||
- 对于复杂对象,先用 `JSON.parse(JSON.stringify(obj))` 转换
|
||
|
||
================================================================================
|
||
四十四、字符串不能直接作为布尔条件(重要)
|
||
================================================================================
|
||
|
||
1. 字符串不能直接作 if 条件
|
||
- UTS Android 不支持将字符串直接作为布尔条件判断
|
||
- 错误示例:
|
||
```typescript
|
||
const paramId = '123'
|
||
if (paramId) { // 错误:字符串不能直接作为布尔条件
|
||
// ...
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const paramId = '123'
|
||
if (paramId != null && paramId != '') { // 正确:显式判断
|
||
// ...
|
||
}
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"Condition type mismatch: inferred type is 'String' but 'Boolean' was expected"
|
||
- 原因:字符串类型不能直接作为布尔条件
|
||
- 解决:使用显式的比较表达式
|
||
|
||
3. 最佳实践
|
||
- 使用 `!= null && != ''` 检查字符串非空
|
||
- 使用 `== null || == ''` 检查字符串为空
|
||
|
||
================================================================================
|
||
四十五、联合类型属性访问(重要)
|
||
================================================================================
|
||
|
||
1. 联合类型不能直接访问属性
|
||
- 当参数类型为联合类型(如 `A | B`)时,不能直接访问属性
|
||
- 错误示例:
|
||
```typescript
|
||
type A = { id: string, name: string }
|
||
type B = { id: string, title: string }
|
||
|
||
const foo = (item: A | B) => {
|
||
const id = item.id // 错误:联合类型不能直接访问属性
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const foo = (item: A | B) => {
|
||
// 方案1:转换为 UTSJSONObject
|
||
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const id = obj.getString('id') ?? ''
|
||
|
||
// 方案2:使用类型守卫
|
||
if ('name' in item) {
|
||
const id = item.id // 此时类型已收窄为 A
|
||
}
|
||
}
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx'"
|
||
- 原因:联合类型的属性访问受限
|
||
- 解决:转换为 UTSJSONObject 或使用类型守卫
|
||
|
||
3. 最佳实践
|
||
- 对于联合类型参数,统一转换为 UTSJSONObject 处理
|
||
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||
|
||
================================================================================
|
||
四十六、any 类型变量不能赋值为 null(重要)
|
||
================================================================================
|
||
|
||
1. any 类型变量不能赋值为 null
|
||
- UTS Android 中 `any` 类型不能赋值为 `null`
|
||
- 错误示例:
|
||
```typescript
|
||
let res: any = null // 错误:Null cannot be a value of a non-null type 'Any'
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
let res: any = {} // 正确:使用空对象
|
||
// 或者
|
||
let res: any | null = null // 使用联合类型
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"Null cannot be a value of a non-null type 'Any'"
|
||
- 原因:any 类型不允许 null 值
|
||
- 解决:使用空对象 `{}` 或联合类型 `any | null`
|
||
|
||
================================================================================
|
||
四十七、对象字面量类型推断问题(重要)
|
||
================================================================================
|
||
|
||
1. 对象字面量直接赋值给 ref 可能类型不匹配
|
||
- 当对象字面量直接赋值给特定类型的 ref 时,可能报类型不匹配错误
|
||
- 错误示例:
|
||
```typescript
|
||
merchant.value = {
|
||
id: shop.id,
|
||
user_id: shop.merchant_id,
|
||
// ...
|
||
} // 错误:Assignment type mismatch
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 方案1:显式声明类型
|
||
const merchantData: MerchantType = {
|
||
id: shop.id,
|
||
user_id: shop.merchant_id,
|
||
// ...
|
||
}
|
||
merchant.value = merchantData
|
||
|
||
// 方案2:使用 as 类型断言
|
||
merchant.value = {
|
||
id: shop.id,
|
||
user_id: shop.merchant_id,
|
||
// ...
|
||
} as MerchantType
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"Assignment type mismatch: actual type is '<anonymous>', but 'XXX' was expected"
|
||
- 原因:对象字面量被推断为匿名类型
|
||
- 解决:显式声明类型或使用类型断言
|
||
|
||
================================================================================
|
||
四十八、any 类型不能直接访问属性(重要)
|
||
================================================================================
|
||
|
||
1. any 类型参数不能直接访问属性
|
||
- 在 map、forEach 等回调中,any 类型的参数不能直接访问属性
|
||
- 错误示例:
|
||
```typescript
|
||
const list = rawList.map((item): ProductType => {
|
||
const id = item.id // 错误:找不到名称 "id"
|
||
const name = item.name // 错误:找不到名称 "name"
|
||
})
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const list = rawList.map((item: any): ProductType => {
|
||
// 方案1:转换为 UTSJSONObject
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const id = itemObj.getString('id') ?? ''
|
||
const name = itemObj.getString('name') ?? ''
|
||
|
||
// 方案2:显式标注参数类型并使用索引
|
||
// 注意:这种方式在 UTS Android 中也可能有问题
|
||
})
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"找不到名称 xxx'"
|
||
- 原因:any 类型的属性访问受限
|
||
- 解决:转换为 UTSJSONObject 后使用 `.getString()` 等方法
|
||
|
||
================================================================================
|
||
四十九、类型断言不会添加方法(重要)
|
||
================================================================================
|
||
|
||
1. `as UTSJSONObject` 不会给对象添加方法
|
||
- 使用 `as UTSJSONObject` 只是类型断言,不会让普通对象获得 `getString` 等方法
|
||
- 错误示例:
|
||
```typescript
|
||
const profileObj = profile as UTSJSONObject
|
||
const id = profileObj.getString('user_id') // 运行时错误:getString is not a function
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 必须使用 JSON.parse(JSON.stringify()) 进行真正的转换
|
||
const profileObj = JSON.parse(JSON.stringify(profile)) as UTSJSONObject
|
||
const id = profileObj.getString('user_id') ?? ''
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"XXX is not a function"
|
||
- 原因:类型断言只是编译时行为,不会改变运行时对象的方法
|
||
- 解决:使用 `JSON.parse(JSON.stringify())` 进行真正的对象转换
|
||
|
||
3. 最佳实践
|
||
- 对于从 API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||
- 使用 `instanceof UTSJSONObject` 检查对象类型
|
||
- 不要依赖 `as` 类型断言来添加方法
|
||
|
||
================================================================================
|
||
五十、类型必须包含所有必填字段(重要)
|
||
================================================================================
|
||
|
||
1. 创建类型实例时必须包含所有必填字段
|
||
- UTS 类型定义中的非可选字段(不带 `?`)都是必填的
|
||
- 错误示例:
|
||
```typescript
|
||
export type ProductType = {
|
||
id: string
|
||
merchant_id: string // 必填
|
||
category_id: string // 必填
|
||
name: string
|
||
// ...
|
||
}
|
||
|
||
// 错误:缺少 merchant_id、category_id 等必填字段
|
||
return {
|
||
id: item.id,
|
||
name: item.name,
|
||
price: item.price
|
||
} as ProductType // 运行时错误:missing required property
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
return {
|
||
id: itemObj.getString('id') ?? '',
|
||
merchant_id: itemObj.getString('merchant_id') ?? '',
|
||
category_id: itemObj.getString('category_id') ?? '',
|
||
name: itemObj.getString('name') ?? '未知商品',
|
||
description: itemObj.getString('description') ?? '',
|
||
images: images,
|
||
price: itemObj.getNumber('base_price') ?? 0,
|
||
original_price: itemObj.getNumber('market_price') ?? 0,
|
||
stock: itemObj.getNumber('total_stock') ?? 0,
|
||
sales: itemObj.getNumber('sale_count') ?? 0,
|
||
status: 1,
|
||
created_at: itemObj.getString('created_at') ?? ''
|
||
} as ProductType
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"Failed to construct type, missing required property: xxx"
|
||
- 原因:类型定义中有必填字段未提供
|
||
- 解决:
|
||
1. 检查类型定义,确认哪些字段是必填的(不带 `?`)
|
||
2. 为所有必填字段提供值,即使是空字符串或默认值
|
||
|
||
3. 最佳实践
|
||
- 查看类型定义,确认哪些字段是必填的(不带 `?`)
|
||
- 使用 `??` 运算符提供默认值
|
||
- 对于可选字段,可以不提供或使用 `null`
|
||
|
||
================================================================================
|
||
五十一、回调函数不能是 async(重要)
|
||
================================================================================
|
||
|
||
1. API 回调函数不能使用 async 修饰
|
||
- uni API 的回调函数(如 showModal 的 success)不支持 async 函数
|
||
- 错误示例:
|
||
```typescript
|
||
uni.showModal({
|
||
title: '确认',
|
||
content: '确定要删除吗?',
|
||
success: async (res) => { // 错误:回调函数不能是 async
|
||
if (res.confirm) {
|
||
const result = await someAsyncFunction()
|
||
}
|
||
}
|
||
})
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
uni.showModal({
|
||
title: '确认',
|
||
content: '确定要删除吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// 使用 Promise.then() 代替 await
|
||
someAsyncFunction().then((result) => {
|
||
// 处理结果
|
||
})
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"参数类型不匹配:实际类型为 'Function1<..., UTSPromise<Unit>>',预期类型为 'Function1<..., Unit>?'"
|
||
- 原因:回调函数返回 Promise 而非 void
|
||
- 解决:使用 `.then()` 代替 `await`
|
||
|
||
3. 最佳实践
|
||
- 在回调函数中使用 `.then()` 处理异步操作
|
||
- 将异步逻辑封装为单独的函数,在回调中调用
|
||
|
||
================================================================================
|
||
五十二、类型转换前必须检查类型(重要)
|
||
================================================================================
|
||
|
||
1. 使用 `as` 类型转换前必须检查实际类型
|
||
- 直接使用 `as string` 转换可能导致运行时类型转换异常
|
||
- 错误示例:
|
||
```typescript
|
||
const idVal = item['id']
|
||
const id = idVal as string // 错误:如果 idVal 是其他类型会崩溃
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const idVal = item['id']
|
||
const id = (idVal != null && typeof idVal == 'string') ? (idVal as string) : ''
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"null cannot be cast to non-null type kotlin.String"
|
||
- 错误信息:"java.lang.Boolean cannot be cast to java.lang.String"
|
||
- 原因:直接类型转换时,实际类型与目标类型不匹配
|
||
- 解决:使用 `typeof` 检查类型后再转换
|
||
|
||
3. 最佳实践
|
||
- 使用 `typeof` 检查类型
|
||
- 使用 `!= null` 检查空值
|
||
- 提供默认值防止空指针异常
|
||
|
||
================================================================================
|
||
五十三、UTSJSONObject 必须正确转换(重要)
|
||
================================================================================
|
||
|
||
1. `as UTSJSONObject` 不会添加方法
|
||
- 从数据库返回的数据需要正确转换为 UTSJSONObject
|
||
- 错误示例:
|
||
```typescript
|
||
const item = rawList[i]
|
||
const brandObj = item as UTSJSONObject // 错误:brandObj.getString 不存在
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const item = rawList[i]
|
||
const brandObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const id = brandObj.getString('id') ?? ''
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"getString is not a function"
|
||
- 原因:对象没有正确转换为 UTSJSONObject
|
||
- 解决:使用 `JSON.parse(JSON.stringify())` 进行转换
|
||
|
||
3. 最佳实践
|
||
- 对于从数据库/API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||
|
||
================================================================================
|
||
五十四、getBoolean 方法可能导致类型转换异常(重要)
|
||
================================================================================
|
||
|
||
1. `UTSJSONObject.getBoolean()` 可能导致类型转换异常
|
||
- 当数据库字段类型与预期不符时,getBoolean() 可能抛出异常
|
||
- 错误示例:
|
||
```typescript
|
||
const isFeatured = prodObj.getBoolean('is_featured') ?? false // 可能抛出异常
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const isFeaturedVal = prodObj.get('is_featured')
|
||
const isFeatured = (isFeaturedVal != null && typeof isFeaturedVal == 'boolean')
|
||
? (isFeaturedVal as boolean)
|
||
: false
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"java.lang.Boolean cannot be cast to java.lang.String"
|
||
- 原因:数据库返回的字段类型与 UTSJSONObject 方法期望的类型不匹配
|
||
- 解决:使用 `.get()` 方法获取原始值,然后手动检查类型
|
||
|
||
3. 最佳实践
|
||
- 避免使用 `.getBoolean()`,改用 `.get()` + `typeof` 检查
|
||
- 在 SQL 查询中明确指定需要的字段,避免 `SELECT *`
|
||
- 对于布尔值,使用 `typeof val == 'boolean'` 检查类型
|
||
|
||
================================================================================
|
||
五十五、SELECT * 可能导致类型转换问题(重要)
|
||
================================================================================
|
||
|
||
1. 避免使用 `SELECT *` 查询所有字段
|
||
- 数据库可能包含前端不需要的字段,导致类型转换异常
|
||
- 错误示例:
|
||
```typescript
|
||
.select('*') // 可能返回意外的字段类型
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
.select('id, name, description, base_price, market_price, main_image_url')
|
||
```
|
||
|
||
2. 最佳实践
|
||
- 只查询需要的字段
|
||
- 参考数据库文档确认字段类型
|
||
- 对于视图(如 `ml_products_detail_view`),注意字段名可能与基础表不同
|
||
|
||
================================================================================
|
||
五十六、创建辅助函数处理数据转换(重要)
|
||
================================================================================
|
||
|
||
1. 创建辅助函数统一处理数据类型转换
|
||
- 避免在每个方法中重复写类型检查代码
|
||
- 示例:
|
||
```typescript
|
||
// 辅助函数:安全获取字符串值
|
||
function safeGetString(obj: UTSJSONObject, key: string): string {
|
||
try {
|
||
const val = obj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val as string
|
||
if (typeof val == 'number') return (val as number).toString()
|
||
if (typeof val == 'boolean') return (val as boolean) ? 'true' : 'false'
|
||
return ''
|
||
} catch (e) {
|
||
console.error('safeGetString error for key:', key, e)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
// 辅助函数:安全获取数值
|
||
function safeGetNumber(obj: UTSJSONObject, key: string): number {
|
||
try {
|
||
const val = obj.get(key)
|
||
if (val == null) return 0
|
||
if (typeof val == 'number') return val as number
|
||
if (typeof val == 'string') {
|
||
const parsed = parseFloat(val as string)
|
||
return isNaN(parsed) ? 0 : parsed
|
||
}
|
||
return 0
|
||
} catch (e) {
|
||
console.error('safeGetNumber error for key:', key, e)
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// 辅助函数:安全获取布尔值
|
||
function safeGetBoolean(obj: UTSJSONObject, key: string): boolean {
|
||
try {
|
||
const val = obj.get(key)
|
||
if (val == null) return false
|
||
if (typeof val == 'boolean') return val as boolean
|
||
if (typeof val == 'string') return (val as string) === 'true'
|
||
if (typeof val == 'number') return (val as number) !== 0
|
||
return false
|
||
} catch (e) {
|
||
console.error('safeGetBoolean error for key:', key, e)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 辅助函数:从原始数据解析商品
|
||
function parseProductFromRaw(item: any): Product {
|
||
const prodObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
return {
|
||
id: safeGetString(prodObj, 'id'),
|
||
name: safeGetString(prodObj, 'name'),
|
||
base_price: safeGetNumber(prodObj, 'base_price'),
|
||
is_featured: safeGetBoolean(prodObj, 'is_featured'),
|
||
// ...
|
||
} as Product
|
||
}
|
||
```
|
||
|
||
2. 优点
|
||
- 代码复用,减少重复
|
||
- 统一处理各种类型转换异常
|
||
- 易于维护和修改
|
||
|
||
3. 最佳实践
|
||
- 将辅助函数放在文件顶部
|
||
- 对所有从数据库获取的数据使用辅助函数
|
||
- 处理所有可能的类型转换情况
|
||
|
||
================================================================================
|
||
五十七、视图字段名可能与基础表不同(重要)
|
||
================================================================================
|
||
|
||
1. 数据库视图的字段名可能与基础表不同
|
||
- `ml_products_detail_view` 视图中没有 `image_url` 字段
|
||
- 只有 `main_image_url` 和 `image_urls` 字段
|
||
- 错误示例:
|
||
```typescript
|
||
.select('id, name, image_url') // 错误:视图没有 image_url 字段
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
.select('id, name, main_image_url, image_urls') // 正确
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"column ml_products_detail_view.image_url does not exist"
|
||
- 提示:"Perhaps you meant to reference the column 'ml_products_detail_view.image_urls'"
|
||
- 原因:查询了视图中不存在的字段
|
||
- 解决:参考数据库文档确认视图字段名
|
||
|
||
3. 最佳实践
|
||
- 查询视图前先确认字段名
|
||
- 使用明确字段列表而非 `SELECT *`
|
||
|
||
================================================================================
|
||
五十八、辅助函数应使用 try-catch 包装(重要)
|
||
================================================================================
|
||
|
||
1. 辅助函数应使用 try-catch 包装防止崩溃
|
||
- 在 Android 端,类型转换可能抛出异常
|
||
- 使用 try-catch 包装可以防止整个应用崩溃
|
||
- 示例:
|
||
```typescript
|
||
function safeGetString(obj: UTSJSONObject, key: string): string {
|
||
try {
|
||
const val = obj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val as string
|
||
if (typeof val == 'number') return (val as number).toString()
|
||
if (typeof val == 'boolean') return (val as boolean) ? 'true' : 'false'
|
||
return ''
|
||
} catch (e) {
|
||
console.error('safeGetString error for key:', key, e)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
function toUTSJSONObject(item: any): UTSJSONObject {
|
||
if (item instanceof UTSJSONObject) {
|
||
return item as UTSJSONObject
|
||
}
|
||
try {
|
||
const str = JSON.stringify(item)
|
||
return JSON.parse(str) as UTSJSONObject
|
||
} catch (e) {
|
||
console.error('toUTSJSONObject error:', e)
|
||
return new UTSJSONObject()
|
||
}
|
||
}
|
||
```
|
||
|
||
2. 优点
|
||
- 防止单个字段解析失败导致整个应用崩溃
|
||
- 提供详细的错误日志便于调试
|
||
- 返回默认值保证应用继续运行
|
||
|
||
3. 最佳实践
|
||
- 所有辅助函数都应使用 try-catch 包装
|
||
- 在 catch 中记录错误日志
|
||
- 返回合理的默认值
|
||
|
||
================================================================================
|
||
五十九、商品视图没有 shop_id 字段(重要)
|
||
================================================================================
|
||
|
||
1. `ml_products_detail_view` 视图没有 `shop_id` 字段
|
||
- 商品通过 `merchant_id` 关联商家/店铺
|
||
- 错误示例:
|
||
```typescript
|
||
.select('id, name, shop_id') // 错误:视图没有 shop_id 字段
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
.select('id, name, merchant_id') // 正确:使用 merchant_id
|
||
```
|
||
|
||
2. 数据库字段对应关系
|
||
- `ml_products` 表:`merchant_id` 关联商家
|
||
- `ml_shops` 表:`user_id` 等于商家的 `merchant_id`
|
||
- 视图中通过 `merchant_id` JOIN `ml_shops` 获取店铺信息
|
||
|
||
3. 最佳实践
|
||
- 查询商品时使用 `merchant_id` 而非 `shop_id`
|
||
|
||
================================================================================
|
||
六十、any 类型不支持索引访问(重要)
|
||
================================================================================
|
||
|
||
1. UTS Android 中 `any` 类型不支持索引访问
|
||
- 不能使用 `obj[key]` 语法访问 `any` 类型对象的属性
|
||
- 错误示例:
|
||
```typescript
|
||
function safeGetString(obj: any, key: string): string {
|
||
const val = obj[key] // 错误:unresolved reference
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
function safeGetString(obj: UTSJSONObject, key: string): string {
|
||
const val = obj.get(key) // 正确:使用 UTSJSONObject 的 get 方法
|
||
}
|
||
```
|
||
|
||
2. 编译错误提示
|
||
- 错误信息:"Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch"
|
||
- 原因:`any` 类型不支持索引访问
|
||
- 解决:先将对象转换为 `UTSJSONObject`,再使用 `.get()` 方法
|
||
|
||
3. 最佳实践
|
||
- 使用 `toUTSJSONObject()` 函数将 `any` 转换为 `UTSJSONObject`
|
||
- 使用 `.get()`、`.getString()`、`.getNumber()` 等方法访问属性
|
||
- 辅助函数参数类型应为 `UTSJSONObject` 而非 `any`
|
||
|
||
================================================================================
|
||
六十一、使用 getString/getNumber/getBoolean 方法(重要)
|
||
================================================================================
|
||
|
||
1. UTSJSONObject 提供了类型安全的访问方法
|
||
- `getString(key)` - 直接返回字符串或 null
|
||
- `getNumber(key)` - 直接返回数值或 null
|
||
- `getBoolean(key)` - 直接返回布尔值或 null
|
||
- `getArray(key)` - 直接返回数组或 null
|
||
- 这些方法比 `.get()` 更安全,会自动进行类型转换
|
||
|
||
2. 推荐用法:
|
||
```typescript
|
||
// 推荐使用
|
||
const name = obj.getString('name') ?? ''
|
||
const price = obj.getNumber('price') ?? 0
|
||
const isActive = obj.getBoolean('is_active') ?? false
|
||
const images = obj.getArray('images') as string[] ?? []
|
||
|
||
// 不推荐使用 .get() 后手动类型检查
|
||
const val = obj.get('key')
|
||
if (typeof val == 'string') { ... }
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 优先使用 `getString()`、`getNumber()`、`getBoolean()`、`getArray()`
|
||
- 使用 `??` 提供默认值
|
||
- 在 catch 块中处理异常
|
||
|
||
================================================================================
|
||
六十二、item as UTSJSONObject 不会添加方法(重要)
|
||
================================================================================
|
||
|
||
1. `item as UTSJSONObject` 不会让对象获得 `getString` 等方法
|
||
- 直接使用 `as UTSJSONObject` 只是类型断言,不会改变运行时对象
|
||
- 错误示例:
|
||
```typescript
|
||
const item = rawList[i]
|
||
const prodObj = item as UTSJSONObject // 错误:getString 不存在
|
||
const id = prodObj.getString('id') // 运行时错误
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const item = rawList[i]
|
||
const prodObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const id = prodObj.getString('id') ?? '' // 正确
|
||
```
|
||
|
||
2. 运行时错误提示
|
||
- 错误信息:"getString is not a function"
|
||
- 原因:对象没有正确转换为 UTSJSONObject
|
||
- 解决:使用 `JSON.parse(JSON.stringify())` 进行转换
|
||
|
||
3. 最佳实践
|
||
- 对于从数据库/API 返回的数据,统一使用 `JSON.parse(JSON.stringify())` 转换
|
||
- 使用 `.getString()`、`.getNumber()` 等方法安全访问属性
|
||
|
||
================================================================================
|
||
六十三、自定义导航栏 Android 端适配(重要)
|
||
================================================================================
|
||
|
||
1. .smart-navbar 样式规范
|
||
- 问题:在 Android 端沉浸式导航栏中,若 .smart-navbar 设置了 paddingTop: statusBarHeight,且 flex-direction 未显式设置为 column,内部元素(标题、按钮)可能会错位。
|
||
- 修复:.smart-navbar 必须显式设置 flex-direction: column,内部内容容器 .nav-container 设置为 flex-direction: row。
|
||
- 正确示例:
|
||
```css
|
||
.smart-navbar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
}
|
||
.nav-container {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 44px;
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
六十四、常见修复模式速查
|
||
================================================================================
|
||
|
||
1. 取反操作修复模式
|
||
```typescript
|
||
// 错误
|
||
if (!variable) { ... }
|
||
if (!isValid.value) { ... }
|
||
if (!validate()) { ... }
|
||
|
||
// 正确 - 根据类型选择
|
||
if (variable == null || variable == '') { ... } // 字符串判空
|
||
if (isValid.value == false) { ... } // 布尔值取反
|
||
if (validate() == false) { ... } // 函数返回布尔值取反
|
||
```
|
||
|
||
2. 解构赋值修复模式
|
||
```typescript
|
||
// 错误
|
||
const { data, error } = await someAsyncCall()
|
||
|
||
// 正确
|
||
const result = await someAsyncCall()
|
||
const data = result.data
|
||
const error = result.error
|
||
```
|
||
|
||
3. typeof 检查修复模式
|
||
```typescript
|
||
// 错误
|
||
if (typeof err === 'object') { ... }
|
||
if (typeof xxx === 'function') { ... }
|
||
|
||
// 正确
|
||
try {
|
||
const e = err as Error
|
||
// 使用 e
|
||
} catch (e2) {
|
||
// 处理转换失败
|
||
}
|
||
```
|
||
|
||
4. as unknown as 修复模式
|
||
```typescript
|
||
// 错误
|
||
const timer = setInterval(...) as unknown as number
|
||
|
||
// 正确
|
||
const timer = setInterval(...) as number
|
||
```
|
||
|
||
================================================================================
|
||
六十五、错误处理最佳实践
|
||
================================================================================
|
||
|
||
1. 统一错误处理模式
|
||
```typescript
|
||
try {
|
||
const result = await someAsyncCall()
|
||
if (result.error != null) {
|
||
const errorMsg = (result.error as Error).message
|
||
uni.showToast({ title: errorMsg, icon: 'none' })
|
||
return
|
||
}
|
||
// 处理成功结果
|
||
} catch (e) {
|
||
console.error('操作失败:', e)
|
||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
```
|
||
|
||
2. 可空类型安全访问
|
||
```typescript
|
||
// 安全访问对象属性
|
||
const value = obj != null ? obj.property : null
|
||
|
||
// 安全调用方法
|
||
const result = obj != null ? obj.method() : null
|
||
```
|
||
|
||
3. 数组安全访问
|
||
```typescript
|
||
// 安全访问数组元素
|
||
if (arr.length > index) {
|
||
const item = arr[index]
|
||
// 使用 item
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
六十六、scroll-view 正确使用方式(重要)
|
||
================================================================================
|
||
|
||
1. scroll-view 属性语法
|
||
- 在 uvue 中,scroll-view 的滚动属性必须使用绑定语法
|
||
- 错误示例:
|
||
```html
|
||
<scroll-view scroll-y class="content">
|
||
<scroll-view direction="vertical" class="content">
|
||
```
|
||
- 正确示例:
|
||
```html
|
||
<scroll-view :scroll-y="true" class="content">
|
||
```
|
||
|
||
2. scroll-view 高度约束
|
||
- scroll-view 必须有明确的高度约束才能正常滚动
|
||
- 父容器需要设置 `flex: 1` 和 `height: 0px`(或 `height: 0`)
|
||
- scroll-view 本身设置 `flex: 1` 和 `height: 100%`
|
||
- 正确示例:
|
||
```css
|
||
.parent-container {
|
||
flex: 1;
|
||
height: 0px; /* 关键:配合 flex: 1 使用 */
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.scroll-content {
|
||
flex: 1;
|
||
height: 100%; /* 继承父容器高度 */
|
||
}
|
||
```
|
||
|
||
3. 常见问题
|
||
- 问题:scroll-view 内容无法滚动
|
||
- 原因:缺少高度约束或 `height: 0px` 未设置
|
||
- 解决:确保父容器有 `height: 0px`,scroll-view 有 `height: 100%`
|
||
|
||
================================================================================
|
||
六十七、switchTab 页面间参数传递(重要)
|
||
================================================================================
|
||
|
||
1. switchTab 不支持 URL 参数
|
||
- `uni.switchTab` 不能像 `uni.navigateTo` 那样传递 URL 参数
|
||
- 错误示例:
|
||
```typescript
|
||
uni.switchTab({
|
||
url: '/pages/category?categoryId=123' // 参数会被忽略
|
||
})
|
||
```
|
||
|
||
2. 使用 Storage 传递参数
|
||
- 正确示例:
|
||
```typescript
|
||
// 发送页面
|
||
uni.setStorageSync('selectedCategory', categoryId)
|
||
uni.switchTab({ url: '/pages/category' })
|
||
|
||
// 接收页面
|
||
onShow(() => {
|
||
const savedCategoryId = uni.getStorageSync('selectedCategory')
|
||
if (savedCategoryId != null && savedCategoryId != '') {
|
||
uni.removeStorageSync('selectedCategory') // 清除,避免重复使用
|
||
// 处理参数
|
||
}
|
||
})
|
||
```
|
||
|
||
3. 时序问题处理
|
||
- 问题:onShow 可能在数据加载完成前执行
|
||
- 解决:使用 pending 变量暂存参数,等待数据加载完成后处理
|
||
- 正确示例:
|
||
```typescript
|
||
const pendingCategoryId = ref('')
|
||
|
||
onShow(() => {
|
||
const savedCategoryId = uni.getStorageSync('selectedCategory')
|
||
if (savedCategoryId != null && savedCategoryId != '') {
|
||
uni.removeStorageSync('selectedCategory')
|
||
|
||
if (primaryCategories.value.length > 0) {
|
||
// 数据已加载,直接处理
|
||
selectPrimaryCategory(savedCategoryId as string)
|
||
} else {
|
||
// 数据未加载,暂存等待
|
||
pendingCategoryId.value = savedCategoryId as string
|
||
}
|
||
}
|
||
})
|
||
|
||
async function loadCategories(): Promise<void> {
|
||
// ... 加载数据 ...
|
||
|
||
// 检查是否有待处理的参数
|
||
if (pendingCategoryId.value != '') {
|
||
const id = pendingCategoryId.value
|
||
pendingCategoryId.value = ''
|
||
selectPrimaryCategory(id)
|
||
}
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
六十八、函数定义顺序严格检查(重要)
|
||
================================================================================
|
||
|
||
1. UTS 中函数必须先定义后使用
|
||
- UTS 不支持 JavaScript 的函数提升
|
||
- 被调用的函数必须在调用之前定义
|
||
- 错误示例:
|
||
```typescript
|
||
// 错误:selectPrimaryCategory 未定义
|
||
async function loadCategories(): Promise<void> {
|
||
// ...
|
||
selectPrimaryCategory(categoryId) // 编译错误
|
||
}
|
||
|
||
async function selectPrimaryCategory(id: string): Promise<void> {
|
||
// ...
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
// 正确:先定义被调用的函数
|
||
async function selectPrimaryCategory(id: string): Promise<void> {
|
||
// ...
|
||
}
|
||
|
||
async function loadCategories(): Promise<void> {
|
||
// ...
|
||
selectPrimaryCategory(categoryId) // 正确
|
||
}
|
||
```
|
||
|
||
2. 变量使用前必须定义
|
||
- 错误示例:
|
||
```typescript
|
||
if (systemInfo.windowWidth > 1025) { // 错误:systemInfo 未定义
|
||
// ...
|
||
}
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
const systemInfo = uni.getSystemInfoSync() // 先定义
|
||
if (systemInfo.windowWidth > 1025) { // 正确
|
||
// ...
|
||
}
|
||
```
|
||
|
||
3. 推荐的代码组织顺序
|
||
```typescript
|
||
<script setup lang="uts">
|
||
// 1. 导入语句
|
||
import { ref, onMounted, onShow } from 'vue'
|
||
|
||
// 2. 类型定义
|
||
type LocalCategory = { id: string, name: string }
|
||
|
||
// 3. 响应式变量声明
|
||
const pendingCategoryId = ref('')
|
||
const primaryCategories = ref<LocalCategory[]>([])
|
||
|
||
// 4. 工具函数(不依赖其他函数)
|
||
const getUserId = (): string => { ... }
|
||
|
||
// 5. 核心业务函数(被其他函数调用)
|
||
async function selectPrimaryCategory(id: string): Promise<void> { ... }
|
||
async function loadProducts(): Promise<void> { ... }
|
||
|
||
// 6. 高层业务函数(调用核心函数)
|
||
async function loadCategories(): Promise<void> {
|
||
// 可以安全调用 selectPrimaryCategory
|
||
}
|
||
|
||
// 7. 生命周期钩子(最后)
|
||
onMounted(() => { ... })
|
||
onShow(() => { ... })
|
||
</script>
|
||
```
|
||
|
||
================================================================================
|
||
六十九、CSS 渐变背景兼容性(重要)
|
||
================================================================================
|
||
|
||
1. linear-gradient 在 uvue 中可能不支持
|
||
- 问题:`background: linear-gradient(...)` 在 Android 端可能不显示
|
||
- 解决:使用纯色背景 `background-color` 替代
|
||
- 错误示例:
|
||
```css
|
||
.navbar {
|
||
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
|
||
}
|
||
```
|
||
- 正确示例:
|
||
```css
|
||
.navbar {
|
||
background-color: #4CAF50;
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
七十、避免重复定义生命周期钩子(重要)
|
||
================================================================================
|
||
|
||
1. 不要重复定义相同的生命周期钩子
|
||
- UTS 中如果定义了两个 `onShow`,它们都会被执行
|
||
- 这会导致逻辑冲突和意外行为
|
||
- 错误示例:
|
||
```typescript
|
||
onShow(() => {
|
||
// 第一个 onShow
|
||
selectPrimaryCategory(id1)
|
||
})
|
||
|
||
onShow(() => {
|
||
// 第二个 onShow 也会执行
|
||
// 可能覆盖第一个的处理结果
|
||
})
|
||
```
|
||
- 正确示例:
|
||
```typescript
|
||
onShow(() => {
|
||
// 合并所有逻辑到一个 onShow 中
|
||
// 处理 Storage 参数
|
||
// 处理页面参数
|
||
// 其他逻辑
|
||
})
|
||
```
|
||
|
||
================================================================================
|
||
七十一、二级分类ID与一级分类ID的处理(重要)
|
||
================================================================================
|
||
|
||
1. 分类页可能收到二级分类ID
|
||
- 首页点击二级分类(如"男装")跳转到分类页
|
||
- 分类页左侧栏显示的是一级分类(如"服装鞋帽")
|
||
- 需要将二级分类ID转换为其父级分类ID
|
||
|
||
2. 处理方案
|
||
```typescript
|
||
async function selectPrimaryCategory(categoryId: string): Promise<void> {
|
||
// 检查传入的是否是一级分类ID
|
||
let targetCategoryId = categoryId
|
||
const foundInPrimary = primaryCategories.value.find(c => c.id === categoryId)
|
||
|
||
if (foundInPrimary == null) {
|
||
// 传入的可能是二级分类ID,查找父级分类
|
||
const categoryInfo = await supabaseService.getCategoryById(categoryId)
|
||
if (categoryInfo != null && categoryInfo.parent_id != null) {
|
||
targetCategoryId = categoryInfo.parent_id
|
||
}
|
||
}
|
||
|
||
activePrimary.value = targetCategoryId
|
||
// ... 后续处理
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 在服务层添加 `getCategoryById` 方法获取单个分类信息
|
||
- 通过 `parent_id` 字段确定父级分类
|
||
- 如果找不到父级,回退到默认分类
|
||
|
||
================================================================================
|
||
七十二、Supabase .single() 方法兼容性(重要)
|
||
================================================================================
|
||
|
||
1. `.single()` 在 UTS 中可能返回不同格式
|
||
- 问题:`.single()` 返回的数据可能导致类型转换异常
|
||
- 错误信息:`UTSArray cannot be cast to UTSJSONObject`
|
||
- 错误示例:
|
||
```typescript
|
||
const response = await supa
|
||
.from('table')
|
||
.select('*')
|
||
.eq('id', id)
|
||
.single()
|
||
.execute()
|
||
|
||
const rawData = response.data
|
||
const obj = JSON.parse(JSON.stringify(rawData)) as UTSJSONObject // 可能报错
|
||
```
|
||
|
||
2. 解决方案:使用 `.limit(1)` 替代
|
||
```typescript
|
||
const response = await supa
|
||
.from('table')
|
||
.select('*')
|
||
.eq('id', id)
|
||
.limit(1) // 使用 limit(1) 替代 single()
|
||
.execute()
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) {
|
||
return null
|
||
}
|
||
|
||
// 作为数组处理
|
||
const rawList = rawData as any[]
|
||
if (rawList.length == 0) {
|
||
return null
|
||
}
|
||
|
||
const item = rawList[0]
|
||
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 避免使用 `.single()`,改用 `.limit(1)`
|
||
- 始终将返回数据作为数组处理
|
||
- 检查数组长度后再取第一个元素
|
||
|
||
================================================================================
|
||
七十三、字符串比较必须使用 == 而不是 ===(重要)
|
||
================================================================================
|
||
|
||
1. UTS Android 中 `===` 比较字符串可能失败
|
||
- 问题:即使两个字符串内容相同,`===` 也可能返回 `false`
|
||
- 这会影响 `find`、`findIndex`、`filter` 等数组方法
|
||
- 错误示例:
|
||
```typescript
|
||
const found = array.find((item) => item.id === targetId) // 可能失败
|
||
const index = array.findIndex((item) => item.id === targetId) // 可能失败
|
||
if (str1 === str2) { ... } // 可能失败
|
||
```
|
||
|
||
2. 解决方案:使用 `==` 或 for 循环
|
||
```typescript
|
||
// 方案1:使用 == 替代 ===
|
||
const found = array.find((item) => item.id == targetId)
|
||
if (str1 == str2) { ... }
|
||
|
||
// 方案2:使用 for 循环(推荐,更可靠)
|
||
let foundItem: Item | null = null
|
||
for (let i = 0; i < array.length; i++) {
|
||
if (array[i].id == targetId) {
|
||
foundItem = array[i]
|
||
break
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 受影响的场景
|
||
- 数组查找:`find`、`findIndex`、`filter`、`some`、`every`
|
||
- 字符串比较:`if (a === b)`
|
||
- 对象属性比较:`if (obj.id === targetId)`
|
||
|
||
4. 最佳实践
|
||
- 在 UTS Android 中,**始终使用 `==` 而不是 `===`** 进行字符串比较
|
||
- 对于关键逻辑,使用 `for` 循环替代数组方法
|
||
- 添加调试日志确认比较结果
|
||
|
||
================================================================================
|
||
七十四、Supabase 查询应在数据库层面过滤(重要)
|
||
================================================================================
|
||
|
||
1. 问题:先获取数据再手动过滤导致数据不足
|
||
- 错误示例:
|
||
```typescript
|
||
// 错误:先获取 limit 条数据,再手动过滤
|
||
const response = await supa
|
||
.from('ml_products')
|
||
.select('*')
|
||
.limit(20)
|
||
.execute()
|
||
|
||
// 手动过滤 category_id
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
if (itemCatId == categoryId) {
|
||
products.push(item)
|
||
}
|
||
}
|
||
// 结果:可能返回 0 个商品,因为匹配的商品不在前 20 条中
|
||
```
|
||
|
||
2. 解决方案:在数据库层面进行过滤
|
||
```typescript
|
||
// 正确:在数据库层面过滤
|
||
const response = await supa
|
||
.from('ml_products')
|
||
.select('*')
|
||
.eq('category_id', categoryId) // 数据库层面过滤
|
||
.eq('status', '1') // 使用字符串 '1'
|
||
.limit(20)
|
||
.execute()
|
||
```
|
||
|
||
3. 需要手动过滤的场景
|
||
- 某些字段(如 `is_hot`、`is_new`、`is_featured`)可能无法在数据库层面过滤
|
||
- 解决方案:获取更多数据(limit * 5),然后手动过滤
|
||
```typescript
|
||
const response = await supa
|
||
.from('ml_products')
|
||
.select('*')
|
||
.eq('status', '1')
|
||
.limit(limit * 5) // 获取更多数据
|
||
.execute()
|
||
|
||
// 手动过滤 is_hot
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
if (isHotBool) {
|
||
products.push(item)
|
||
if (products.length >= limit) break
|
||
}
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 优先在数据库层面使用 `.eq()`、`.in()` 等方法过滤
|
||
- 整数字段使用字符串:`.eq('status', '1')` 而不是 `.eq('status', 1)`
|
||
- 无法在数据库层面过滤时,获取更多数据再手动过滤
|
||
|
||
================================================================================
|
||
七十五、URL 参数类型转换(重要)
|
||
================================================================================
|
||
|
||
1. 问题:URL 参数可能是不同类型
|
||
- 从 URL 传递的参数可能是 `string`、`number` 或其他类型
|
||
- 错误示例:
|
||
```typescript
|
||
const priceOpt = opts['price'] as string // 错误:可能是 number
|
||
this.product.price = parseFloat(priceOpt)
|
||
// 运行时错误:java.lang.Double cannot be cast to java.lang.String
|
||
```
|
||
|
||
2. 解决方案:检查类型后再转换
|
||
```typescript
|
||
const priceOpt = opts['price']
|
||
if (typeof priceOpt == 'number') {
|
||
this.product.price = priceOpt as number
|
||
} else if (typeof priceOpt == 'string') {
|
||
this.product.price = parseFloat(priceOpt as string)
|
||
} else {
|
||
this.product.price = 0
|
||
}
|
||
```
|
||
|
||
3. 通用类型安全获取函数
|
||
```typescript
|
||
function getNumberParam(opts: UTSJSONObject, key: string): number {
|
||
const val = opts.get(key)
|
||
if (typeof val == 'number') return val as number
|
||
if (typeof val == 'string') return parseFloat(val as string)
|
||
return 0
|
||
}
|
||
|
||
function getStringParam(opts: UTSJSONObject, key: string): string {
|
||
const val = opts.get(key)
|
||
if (typeof val == 'string') return val as string
|
||
if (typeof val == 'number') return (val as number).toString()
|
||
return ''
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
七十六、避免使用 .single() 和 .executeAs<T>()(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`.single()` 和 `.executeAs<T>()` 在 UTS Android 中可能失败
|
||
- 错误示例:
|
||
```typescript
|
||
const response = await supa
|
||
.from('ml_products')
|
||
.select('*')
|
||
.eq('id', productId)
|
||
.single()
|
||
.executeAs<Product>()
|
||
// 可能报错:UTSArray cannot be cast to UTSJSONObject
|
||
```
|
||
|
||
2. 解决方案:使用 `.limit(1)` 和手动解析
|
||
```typescript
|
||
const response = await supa
|
||
.from('ml_products')
|
||
.select('*')
|
||
.eq('id', productId)
|
||
.limit(1) // 使用 limit(1) 替代 single()
|
||
.execute()
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return null
|
||
|
||
const rawList = rawData as any[]
|
||
if (rawList.length == 0) return null
|
||
|
||
const item = rawList[0]
|
||
const product = parseProductFromRaw(item) // 使用解析函数
|
||
return product
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 使用 `.limit(1)` 替代 `.single()`
|
||
- 使用 `.execute()` 替代 `.executeAs<T>()`
|
||
- 使用统一的解析函数(如 `parseProductFromRaw`)处理数据
|
||
|
||
================================================================================
|
||
七十七、排序切换状态管理(重要)
|
||
================================================================================
|
||
|
||
1. 问题:点击同一排序按钮时无法切换排序方向
|
||
- 需求:二次点击"价格"时,切换升序/降序
|
||
|
||
2. 解决方案:使用状态变量记录排序方向
|
||
```typescript
|
||
const priceAscending = ref(true) // 价格排序方向
|
||
|
||
const switchSort = (sortId: string) => {
|
||
// 如果点击的是价格排序,切换升序/降序
|
||
if (sortId === 'price' && activeSort.value === 'price') {
|
||
priceAscending.value = !priceAscending.value
|
||
} else {
|
||
// 切换到其他排序时,重置价格排序为升序
|
||
if (sortId !== 'price') {
|
||
priceAscending.value = true
|
||
}
|
||
activeSort.value = sortId
|
||
}
|
||
// 重新加载数据
|
||
loadData()
|
||
}
|
||
```
|
||
|
||
3. 在数据加载时使用排序方向
|
||
```typescript
|
||
switch (activeSort.value) {
|
||
case 'price':
|
||
products = await supabaseService.getProductsByPrice(limit, priceAscending.value)
|
||
break
|
||
// ...
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
七十八、加载更多逻辑(重要)
|
||
================================================================================
|
||
|
||
1. 问题:加载更多时数据被替换而不是追加
|
||
- 错误示例:
|
||
```typescript
|
||
const loadMore = async () => {
|
||
const nextLimit = currentCount + 6
|
||
await loadHotProducts(nextLimit) // 这个函数会替换数据
|
||
}
|
||
```
|
||
|
||
2. 解决方案:根据当前排序方式调用对应函数
|
||
```typescript
|
||
const loadMore = async () => {
|
||
const currentCount = hotProducts.value.length
|
||
const additionalLimit = 6
|
||
|
||
let newProducts: Product[] = []
|
||
switch (activeSort.value) {
|
||
case 'sales':
|
||
newProducts = await supabaseService.getProductsBySales(currentCount + additionalLimit)
|
||
break
|
||
case 'price':
|
||
newProducts = await supabaseService.getProductsByPrice(currentCount + additionalLimit, priceAscending.value)
|
||
break
|
||
// ... 其他排序方式
|
||
}
|
||
|
||
// 检查是否还有更多数据
|
||
if (newProducts.length <= currentCount) {
|
||
hasMore.value = false
|
||
} else {
|
||
hotProducts.value = newProducts
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 每个排序方式对应独立的数据获取函数
|
||
- 传递 `currentCount + additionalLimit` 作为新的 limit
|
||
- 比较返回数量判断是否还有更多数据
|
||
|
||
================================================================================
|
||
七十九、二级分类自适应宽度布局(重要)
|
||
================================================================================
|
||
|
||
1. 问题:二级分类项宽度不一致,超出屏幕
|
||
|
||
2. 解决方案:使用 flex 布局自适应宽度
|
||
```css
|
||
.sub-category-list {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: nowrap;
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.sub-category-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 8px 4px;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
flex: 1; /* 自动扩展填充空间 */
|
||
min-width: 50px; /* 最小宽度保证可读性 */
|
||
}
|
||
```
|
||
|
||
3. 布局效果
|
||
- 二级分类项均匀分布在一行内
|
||
- 根据容器宽度自适应
|
||
- 不会超出屏幕右边
|
||
|
||
================================================================================
|
||
八十、Product 类型与服务层返回类型一致性(重要)
|
||
================================================================================
|
||
|
||
1. 问题:页面期望的数据类型与服务层返回类型不匹配
|
||
- 服务层 `getProductById` 返回 `Product` 类型
|
||
- 页面可能期望 `UTSJSONObject` 类型
|
||
- 导致数据无法正确映射
|
||
|
||
2. 解决方案:统一使用服务层返回的类型
|
||
```typescript
|
||
// 正确:直接使用 Product 对象
|
||
const dbProduct = await supabaseService.getProductById(productId)
|
||
if (dbProduct != null) {
|
||
this.product = {
|
||
id: dbProduct.id,
|
||
name: dbProduct.name,
|
||
stock: dbProduct.stock ?? dbProduct.total_stock ?? 0,
|
||
sales: dbProduct.sale_count ?? 0,
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 类型定义需要包含所有使用到的字段
|
||
- 确保 `Product` 类型包含所有必要字段
|
||
- 如 `specification`、`usage`、`expiry_date` 等
|
||
|
||
================================================================================
|
||
八十一、parseProductFromRaw 辅助函数(重要)
|
||
================================================================================
|
||
|
||
1. 使用统一的解析函数处理数据库返回数据
|
||
```typescript
|
||
function parseProductFromRaw(item: any): Product {
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const getSafeString = (key: string): string => {
|
||
const val = itemObj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val
|
||
if (typeof val == 'number') return val.toString()
|
||
return ''
|
||
}
|
||
|
||
const getSafeNumber = (key: string): number => {
|
||
const val = itemObj.get(key)
|
||
if (val == null) return 0
|
||
if (typeof val == 'number') return val
|
||
if (typeof val == 'string') return parseFloat(val) ?? 0
|
||
return 0
|
||
}
|
||
|
||
return {
|
||
id: getSafeString('id'),
|
||
name: getSafeString('name'),
|
||
// ...
|
||
} as Product
|
||
}
|
||
```
|
||
|
||
2. 优点
|
||
- 统一处理类型转换
|
||
- 避免 `ClassCastException` 错误
|
||
- 提供默认值防止空指针
|
||
|
||
================================================================================
|
||
八十二、点击事件处理(重要)
|
||
================================================================================
|
||
|
||
1. 问题:点击事件条件判断不完整导致无响应
|
||
- 错误示例:
|
||
```typescript
|
||
goToShop() {
|
||
if (this.merchant.user_id != null && this.merchant.user_id !== '') {
|
||
uni.navigateTo({ url: '...' })
|
||
}
|
||
// 如果条件不满足,没有任何反馈
|
||
}
|
||
```
|
||
|
||
2. 解决方案:添加后备方案和用户反馈
|
||
```typescript
|
||
goToShop() {
|
||
const merchantId = this.merchant.id ?? this.product.merchant_id ?? ''
|
||
if (merchantId != '') {
|
||
uni.navigateTo({ url: `/pages/shop?merchantId=${merchantId}` })
|
||
} else {
|
||
uni.showToast({ title: '店铺信息加载中', icon: 'none' })
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 使用 `??` 提供后备值
|
||
- 条件不满足时给用户反馈
|
||
- 添加日志便于调试
|
||
|
||
================================================================================
|
||
八十三、类型定义必须与实际数据类型一致(重要)
|
||
================================================================================
|
||
|
||
1. 问题:类型定义与实际数据类型不匹配
|
||
- `ProductSku.specifications` 定义为 `string`(JSON string)
|
||
- 但代码尝试传入 `UTSJSONObject`
|
||
- 编译错误:`参数类型不匹配:实际类型为 'UTSJSONObject',预期类型为 'String'`
|
||
|
||
2. 解决方案:确保类型一致
|
||
```typescript
|
||
// 错误:specifications 是 string 类型,不能传 UTSJSONObject
|
||
const sku: ProductSku = {
|
||
specifications: specs as UTSJSONObject // 错误
|
||
}
|
||
|
||
// 正确:转换为 JSON string
|
||
let specsStr = ''
|
||
if (rawSpecs != null) {
|
||
if (typeof rawSpecs == 'string') {
|
||
specsStr = rawSpecs as string
|
||
} else {
|
||
specsStr = JSON.stringify(rawSpecs)
|
||
}
|
||
}
|
||
const sku: ProductSku = {
|
||
specifications: specsStr // 正确
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 检查类型定义,确认字段类型
|
||
- JSON 数据存储为 `string` 类型时,需要序列化
|
||
- 使用时再反序列化
|
||
|
||
================================================================================
|
||
八十四、数组类型字段需要正确处理(重要)
|
||
================================================================================
|
||
|
||
1. 问题:数据库存储的 JSON 字符串不能直接赋值给数组类型
|
||
- `Product.tags` 在数据库中是 `string` 类型(JSON 字符串)
|
||
- `ProductType.tags` 是 `Array<string>` 类型
|
||
- 直接赋值导致编译错误
|
||
|
||
2. 解决方案:解析 JSON 字符串后赋值
|
||
```typescript
|
||
// 错误:string 不能直接赋值给 Array<string>
|
||
tags: dbProduct.tags ?? [] as string[] // 错误
|
||
|
||
// 正确:先初始化空数组,再解析 JSON
|
||
tags: [] as string[]
|
||
|
||
// 然后解析
|
||
if (dbProduct.tags != null && dbProduct.tags != '') {
|
||
try {
|
||
const parsedTags = JSON.parse(dbProduct.tags)
|
||
if (Array.isArray(parsedTags)) {
|
||
this.product.tags = (parsedTags as any[]).map((t: any): string => t as string)
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
```
|
||
|
||
3. 常见需要解析的 JSON 字段
|
||
- `tags` - 标签数组
|
||
- `image_urls` - 图片 URL 数组
|
||
- `specifications` - 规格对象
|
||
- `attributes` - 属性对象
|
||
|
||
================================================================================
|
||
八十五、服务层返回类型必须显式解析(重要)
|
||
================================================================================
|
||
|
||
1. 问题:直接使用 `as Type` 强制转换会失败
|
||
- 错误信息:`UTSJSONObject cannot be cast to Shop`
|
||
- 原因:Supabase 返回的是 `UTSJSONObject`,不能直接转换为自定义类型
|
||
|
||
2. 解决方案:使用解析函数显式转换
|
||
```typescript
|
||
// 错误:直接强制转换
|
||
return (response.data as any[])[0] as Shop // 运行时错误
|
||
|
||
// 正确:使用解析函数
|
||
const shopData = (response.data as any[])[0]
|
||
const shop = this.parseShopFromRaw(shopData)
|
||
return shop
|
||
|
||
// 解析函数示例
|
||
parseShopFromRaw(item: any): Shop {
|
||
const itemObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const getSafeString = (key: string): string => {
|
||
const val = itemObj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val == 'string') return val
|
||
return ''
|
||
}
|
||
|
||
return {
|
||
id: getSafeString('id'),
|
||
shop_name: getSafeString('shop_name'),
|
||
// ...
|
||
} as Shop
|
||
}
|
||
```
|
||
|
||
3. 受影响的场景
|
||
- `getShopByMerchantId` - 返回 Shop 类型
|
||
- `fetchShopCoupons` - 返回优惠券数组
|
||
- `getProductSkus` - 返回 SKU 数组
|
||
- 所有从数据库获取并返回自定义类型的函数
|
||
|
||
================================================================================
|
||
八十六、数组遍历不要使用 map 方法(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`map` 方法中的类型转换可能失败
|
||
- 错误信息:`ProductSku cannot be cast to UTSJSONObject`
|
||
- 原因:`map` 回调中的类型推断可能不正确
|
||
|
||
2. 解决方案:使用 `for` 循环替代 `map`
|
||
```typescript
|
||
// 错误:使用 map
|
||
this.productSkus = skus.map((skuData): ProductSkuType => {
|
||
const sku = skuData as UTSJSONObject // 可能失败
|
||
return { ... }
|
||
})
|
||
|
||
// 正确:使用 for 循环
|
||
this.productSkus = []
|
||
for (let i = 0; i < skus.length; i++) {
|
||
const skuData = skus[i]
|
||
// 直接使用 skuData 的属性
|
||
const sku: ProductSkuType = {
|
||
id: skuData.id,
|
||
product_id: skuData.product_id,
|
||
// ...
|
||
}
|
||
this.productSkus.push(sku)
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 避免在数组方法中使用类型转换
|
||
- 使用 `for` 循环手动构建数组
|
||
- 直接访问对象属性而不是强制转换
|
||
|
||
================================================================================
|
||
八十七、类型定义字段必须完全匹配(重要)
|
||
================================================================================
|
||
|
||
1. 问题:构建对象时字段名或类型不匹配
|
||
- 错误信息:`No parameter with name 'xxx' found` 或 `No value passed for parameter 'xxx'`
|
||
- 原因:类型定义中的字段名与代码中使用的不一致
|
||
|
||
2. 解决方案:检查类型定义,确保字段完全匹配
|
||
```typescript
|
||
// 类型定义
|
||
export type CouponTemplateType = {
|
||
id: string
|
||
name: string
|
||
discount_type: number // 注意:是 discount_type 不是 coupon_type
|
||
discount_value: number
|
||
total_quantity: number // 注意:是 total_quantity 不是 total_count
|
||
usage_limit: number // 必填字段
|
||
category_ids: Array<string>
|
||
product_ids: Array<string>
|
||
// ...
|
||
}
|
||
|
||
// 错误:字段名不匹配
|
||
const coupon: CouponTemplateType = {
|
||
usage_scope: 1, // 错误:没有这个字段
|
||
total_count: 100, // 错误:应该是 total_quantity
|
||
// 缺少 usage_limit
|
||
}
|
||
|
||
// 正确:字段完全匹配
|
||
const coupon: CouponTemplateType = {
|
||
id: '...',
|
||
name: '优惠券',
|
||
discount_type: 1,
|
||
discount_value: 10,
|
||
total_quantity: 100,
|
||
usage_limit: 1,
|
||
category_ids: [] as string[],
|
||
product_ids: [] as string[],
|
||
// 所有必填字段都要提供
|
||
}
|
||
```
|
||
|
||
3. 常见错误
|
||
- 字段名拼写错误:`total_count` vs `total_quantity`
|
||
- 缺少必填字段:`usage_limit`、`discount_type`
|
||
- 类型不匹配:`string` vs `number`、`Array<string>` vs `string`
|
||
|
||
4. 最佳实践
|
||
- 在构建对象前,先查看类型定义
|
||
- 确保所有必填字段都有值
|
||
- 数组类型字段初始化为空数组:`[] as string[]`
|
||
|
||
================================================================================
|
||
八十八、服务层返回对象直接使用属性(重要)
|
||
================================================================================
|
||
|
||
1. 问题:尝试将服务层返回的类型对象转换为 UTSJSONObject
|
||
- 错误信息:`Shop cannot be cast to UTSJSONObject`
|
||
- 原因:服务层函数(如 `getShopByMerchantId`)已经返回解析后的类型对象
|
||
|
||
2. 解决方案:直接使用返回对象的属性
|
||
```typescript
|
||
// 错误:尝试转换为 UTSJSONObject
|
||
const shopResponse = await supabaseService.getShopByMerchantId(merchantId)
|
||
const shop = shopResponse as UTSJSONObject // 错误!
|
||
this.merchant = {
|
||
shop_name: shop['shop_name'] as string
|
||
}
|
||
|
||
// 正确:直接使用 Shop 对象的属性
|
||
const shopResponse = await supabaseService.getShopByMerchantId(merchantId)
|
||
if (shopResponse != null) {
|
||
this.merchant = {
|
||
id: shopResponse.id,
|
||
shop_name: shopResponse.shop_name,
|
||
shop_logo: shopResponse.shop_logo ?? '/static/default-shop.png',
|
||
// ...
|
||
} as MerchantType
|
||
}
|
||
```
|
||
|
||
3. 受影响的函数
|
||
- `getShopByMerchantId()` - 返回 `Shop` 类型
|
||
- `getProductById()` - 返回 `Product` 类型
|
||
- `getProductSkus()` - 返回 `ProductSku[]` 类型
|
||
- 所有使用 `parseXxxFromRaw()` 解析的函数
|
||
|
||
4. 最佳实践
|
||
- 检查服务层函数的返回类型
|
||
- 如果返回类型是具体类型(如 `Shop`),直接使用属性
|
||
- 如果返回类型是 `any` 或 `UTSJSONObject`,才需要手动解析
|
||
|
||
================================================================================
|
||
八十九、数组类型返回值必须手动解析(重要)
|
||
================================================================================
|
||
|
||
1. 问题:直接使用 `as Type[]` 转换数组会失败
|
||
- 错误信息:`UTSJSONObject cannot be cast to Notification` 或 `ClassCastException`
|
||
- 原因:Supabase 返回的是 `UTSJSONObject[]`,不能直接转换为自定义类型数组
|
||
|
||
2. 解决方案:使用 for 循环手动解析每个元素
|
||
```typescript
|
||
// 错误:直接转换数组
|
||
return response.data as Notification[] // 运行时错误
|
||
|
||
// 正确:手动解析每个元素
|
||
const rawData = response.data
|
||
if (rawData == null) return []
|
||
|
||
const notifications: Notification[] = []
|
||
const rawList = rawData as any[]
|
||
|
||
for (let i = 0; i < rawList.length; i++) {
|
||
const item = rawList[i]
|
||
const noteObj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
|
||
const note: Notification = {
|
||
id: getSafeString('id'),
|
||
title: getSafeString('title'),
|
||
content: getSafeString('content'),
|
||
is_read: getSafeBoolean('is_read'),
|
||
// ...
|
||
}
|
||
notifications.push(note)
|
||
}
|
||
return notifications
|
||
```
|
||
|
||
3. 受影响的函数
|
||
- `getUserNotifications()` - 返回 `Notification[]`
|
||
- `getChatRooms()` - 返回 `ChatRoom[]`
|
||
- 所有返回自定义类型数组的服务层函数
|
||
|
||
4. 最佳实践
|
||
- 创建 `getSafeString`、`getSafeNumber`、`getSafeBoolean` 辅助函数
|
||
- 使用 `for` 循环而不是 `map` 方法
|
||
- 确保类型定义中的所有字段都被正确赋值
|
||
|
||
================================================================================
|
||
九十、reverse() + map() 组合在 UTS Android 中的问题(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`array.reverse().map()` 组合可能失败
|
||
- `reverse()` 会修改原数组
|
||
- `map()` 中的类型转换可能失败
|
||
- 组合使用时可能导致数据丢失或类型错误
|
||
|
||
2. 解决方案:使用 for 循环反向遍历
|
||
```typescript
|
||
// 错误:使用 reverse() + map()
|
||
messages.value = rawMsgs.reverse().map((m: ChatMessage): UiChatMessage => {
|
||
return {
|
||
id: m.id,
|
||
content: m.content
|
||
}
|
||
})
|
||
|
||
// 正确:使用 for 循环反向遍历
|
||
const uiMessages: UiChatMessage[] = []
|
||
for (let i = rawMsgs.length - 1; i >= 0; i--) {
|
||
const m = rawMsgs[i]
|
||
const uiMsg: UiChatMessage = {
|
||
id: m.id,
|
||
content: m.content ?? ''
|
||
}
|
||
uiMessages.push(uiMsg)
|
||
}
|
||
messages.value = uiMessages
|
||
```
|
||
|
||
3. 优点
|
||
- 不修改原数组
|
||
- 类型安全
|
||
- 更好的性能
|
||
|
||
================================================================================
|
||
九十一、实时订阅中的字符串比较(重要)
|
||
================================================================================
|
||
|
||
1. 问题:实时订阅回调中使用 `===` 和 `!==` 比较字符串失败
|
||
- 症状:收到消息但条件判断失败,消息不显示
|
||
- 原因:`===` 和 `!==` 在 UTS Android 中可能对字符串比较失败
|
||
|
||
2. 解决方案:使用 `==` 和 `!=` 进行字符串比较
|
||
```typescript
|
||
// 错误:使用 === 和 !==
|
||
if (senderId === currentUserId.value) {
|
||
return // 可能失败
|
||
}
|
||
if (receiverId === currentUserId.value) {
|
||
if (senderId !== merchantId.value) {
|
||
return // 可能失败
|
||
}
|
||
}
|
||
|
||
// 正确:使用 == 和 !=
|
||
if (senderId == currentUserId.value) {
|
||
return // 正确工作
|
||
}
|
||
if (receiverId == currentUserId.value) {
|
||
if (senderId != merchantId.value) {
|
||
return // 正确工作
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 调试技巧
|
||
- 在条件判断前添加日志输出变量值
|
||
- 比较字符串时使用 `==` 和 `!=`
|
||
- 确保变量不为 null 或 undefined
|
||
|
||
================================================================================
|
||
九十二、多端实时同步消息处理(重要)
|
||
================================================================================
|
||
|
||
1. 问题:多端同时在线时,自己发送的消息在其他端不显示
|
||
- 原因:实时订阅中跳过了自己发送的消息
|
||
- 错误逻辑:`if (senderId == currentUserId.value) return`
|
||
|
||
2. 解决方案:正确处理自己发送的消息
|
||
```typescript
|
||
// 错误:跳过自己发送的消息
|
||
if (senderId == currentUserId.value) {
|
||
return // 导致其他端看不到自己发的消息
|
||
}
|
||
|
||
// 正确:判断消息类型并显示
|
||
const isMyMessage = (senderId == currentUserId.value)
|
||
const isForMe = (receiverId == currentUserId.value)
|
||
|
||
if (isMyMessage || isForMe) {
|
||
const incomingMsg: UiChatMessage = {
|
||
id: msgId,
|
||
type: isMyMessage ? 'sent' : 'received', // 区分发送/接收
|
||
content: newMsg.getString('content') ?? '',
|
||
time: timeStr
|
||
}
|
||
messages.value.push(incomingMsg)
|
||
}
|
||
```
|
||
|
||
3. 避免重复消息
|
||
- 发送消息时不要使用乐观更新
|
||
- 等待实时订阅推送消息
|
||
- 检查消息 ID 是否已存在
|
||
|
||
```typescript
|
||
// 错误:乐观更新 + 实时订阅 = 重复消息
|
||
messages.value.push(newMessage) // 乐观更新
|
||
await supabaseService.sendMessage(...) // 实时订阅会再推送一条
|
||
|
||
// 正确:只依赖实时订阅
|
||
await supabaseService.sendMessage(...) // 实时订阅推送消息
|
||
|
||
// 检查重复
|
||
for (let i = 0; i < messages.value.length; i++) {
|
||
if (messages.value[i].id == msgId) {
|
||
return // 已存在,跳过
|
||
}
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 统一使用实时订阅处理所有消息(包括自己发送的)
|
||
- 发送消息后不进行乐观更新
|
||
- 通过消息 ID 去重
|
||
|
||
================================================================================
|
||
九十三、避免使用 .single() 和 forEach(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`.single()` 和 `forEach` 在 UTS Android 中可能失败
|
||
- `.single()` 可能导致类型转换错误
|
||
- `forEach` 中的类型判断可能失败
|
||
|
||
2. 解决方案:使用 `.limit(1)` 和 `for` 循环
|
||
```typescript
|
||
// 错误:使用 .single()
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.select('*')
|
||
.eq('id', orderId)
|
||
.single()
|
||
.execute()
|
||
return response.data
|
||
|
||
// 正确:使用 .limit(1)
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.select('*')
|
||
.eq('id', orderId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
const rawList = response.data as any[]
|
||
if (rawList.length == 0) return null
|
||
return rawList[0]
|
||
```
|
||
|
||
3. forEach 替换为 for 循环
|
||
```typescript
|
||
// 错误:使用 forEach
|
||
notes.forEach((note: Notification) => {
|
||
if (note.type === 'order') { // === 可能失败
|
||
orderMessages.push(item)
|
||
}
|
||
})
|
||
|
||
// 正确:使用 for 循环
|
||
for (let i = 0; i < notes.length; i++) {
|
||
const note = notes[i]
|
||
if (note.type == 'order') { // 使用 ==
|
||
orderMessages.push(item)
|
||
}
|
||
}
|
||
```
|
||
|
||
================================================================================
|
||
九十四、嵌套对象数据解析(重要)
|
||
================================================================================
|
||
|
||
1. 问题:Supabase 返回的嵌套对象无法直接访问
|
||
- `dataObj['ml_order_items']` 可能返回 undefined
|
||
- `dataObj['shipping_address']` 类型转换失败
|
||
|
||
2. 解决方案:使用 `JSON.parse(JSON.stringify())` 和 `UTSJSONObject.get()`
|
||
```typescript
|
||
// 错误:直接访问属性
|
||
const dataObj = data as Record<string, any>
|
||
const items = dataObj['ml_order_items'] // 可能失败
|
||
orderItems.value = items as OrderItemType[]
|
||
|
||
// 正确:使用 UTSJSONObject 解析
|
||
const dataObj = JSON.parse(JSON.stringify(data)) as UTSJSONObject
|
||
|
||
// 解析嵌套数组
|
||
const itemsRaw = dataObj.get('ml_order_items')
|
||
if (itemsRaw != null && Array.isArray(itemsRaw)) {
|
||
const items = itemsRaw as any[]
|
||
orderItems.value = []
|
||
for (let i = 0; i < items.length; i++) {
|
||
const itemObj = JSON.parse(JSON.stringify(items[i])) as UTSJSONObject
|
||
const orderItem: OrderItemType = {
|
||
id: (itemObj.get('id') ?? '') as string,
|
||
product_name: (itemObj.get('product_name') ?? '') as string,
|
||
// ...
|
||
}
|
||
orderItems.value.push(orderItem)
|
||
}
|
||
}
|
||
|
||
// 解析嵌套对象
|
||
const addressRaw = dataObj.get('shipping_address')
|
||
if (addressRaw != null) {
|
||
const addressObj = JSON.parse(JSON.stringify(addressRaw)) as UTSJSONObject
|
||
deliveryAddress.value = {
|
||
name: (addressObj.get('name') ?? '') as string,
|
||
phone: (addressObj.get('phone') ?? '') as string,
|
||
// ...
|
||
} as AddressType
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 所有嵌套数据都需要显式解析
|
||
- 使用 `??` 提供默认值
|
||
- 添加日志确认数据解析成功
|
||
|
||
================================================================================
|
||
九十五、避免使用 .single() 查询单条数据(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`.single()` 在 UTS Android 中可能导致类型转换错误
|
||
- 错误信息:`UTSArray cannot be cast to UTSJSONObject`
|
||
- 原因:`.single()` 返回的数据格式与预期不符
|
||
|
||
2. 解决方案:使用 `.limit(1)` 替代
|
||
```typescript
|
||
// 错误:使用 .single()
|
||
const response = await supa
|
||
.from('ml_user_profiles')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.single()
|
||
.execute()
|
||
return response.data
|
||
|
||
// 正确:使用 .limit(1)
|
||
const response = await supa
|
||
.from('ml_user_profiles')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.limit(1)
|
||
.execute()
|
||
|
||
const rawData = response.data
|
||
if (rawData == null) return null
|
||
|
||
const rawList = rawData as any[]
|
||
if (rawList.length == 0) return null
|
||
|
||
return rawList[0]
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 所有查询单条数据的场景都应使用 `.limit(1)`
|
||
- 始终将返回数据作为数组处理
|
||
- 检查数组长度后再取第一个元素
|
||
|
||
================================================================================
|
||
九十六、避免使用 .map() 处理数组转换(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`.map()` 中的类型转换可能在 Android 端失败
|
||
- 错误信息:类型转换异常或数据丢失
|
||
- 原因:`.map()` 回调中的类型推断可能不正确
|
||
|
||
2. 解决方案:使用 `for` 循环替代
|
||
```typescript
|
||
// 错误:使用 .map()
|
||
const products = rawData.map((item: any): Product => {
|
||
const obj = item as UTSJSONObject
|
||
return {
|
||
id: obj.getString('id') ?? '',
|
||
name: obj.getString('name') ?? ''
|
||
} as Product
|
||
})
|
||
|
||
// 正确:使用 for 循环
|
||
const products: Product[] = []
|
||
for (let i = 0; i < rawData.length; i++) {
|
||
const item = rawData[i]
|
||
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||
const product: Product = {
|
||
id: obj.getString('id') ?? '',
|
||
name: obj.getString('name') ?? ''
|
||
} as Product
|
||
products.push(product)
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 所有数组转换都应使用 `for` 循环
|
||
- 使用 `JSON.parse(JSON.stringify())` 确保正确转换为 UTSJSONObject
|
||
- 显式声明结果数组类型
|
||
|
||
================================================================================
|
||
九十七、scroll-view 必须正确配置(重要)
|
||
================================================================================
|
||
|
||
1. 问题:scroll-view 在 Android 端无法滚动
|
||
- 原因1:使用了错误的属性语法 `scroll-y` 而不是 `:scroll-y="true"`
|
||
- 原因2:缺少高度约束
|
||
|
||
2. 解决方案
|
||
```html
|
||
<!-- 错误 -->
|
||
<scroll-view scroll-y class="content">
|
||
|
||
<!-- 正确 -->
|
||
<scroll-view :scroll-y="true" class="content">
|
||
```
|
||
|
||
```css
|
||
/* 父容器 */
|
||
.parent-container {
|
||
flex: 1;
|
||
height: 0px; /* 关键:配合 flex: 1 使用 */
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* scroll-view */
|
||
.content {
|
||
flex: 1;
|
||
height: 0px; /* 关键:确保有高度约束 */
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- scroll-view 的 scroll-y 属性必须使用绑定语法 `:scroll-y="true"`
|
||
- 父容器和 scroll-view 都需要设置 `flex: 1` 和 `height: 0px`
|
||
- 确保 scroll-view 有明确的高度约束
|
||
|
||
================================================================================
|
||
九十八、Record<string, any> 访问属性(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`any` 类型无法直接访问属性
|
||
- 错误信息:`找不到名称 "xxx"`
|
||
- 原因:`any` 类型不支持直接属性访问
|
||
|
||
2. 解决方案:转换为 `Record<string, any>` 后使用索引访问
|
||
```typescript
|
||
// 错误:直接访问 any 类型属性
|
||
const order = orders[i] as any
|
||
const status = order.status // 错误!
|
||
|
||
// 正确:转换为 Record<string, any>
|
||
const order = orders[i] as Record<string, any>
|
||
const status = order['status'] as number // 正确
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 对于需要访问属性的对象,使用 `Record<string, any>` 类型
|
||
- 使用索引访问 `obj['property']` 而非点访问 `obj.property`
|
||
- 访问后使用 `as Type` 进行类型转换
|
||
|
||
================================================================================
|
||
九十九、保留嵌套数据结构(重要)
|
||
================================================================================
|
||
|
||
1. 问题:类型定义不包含嵌套字段导致数据丢失
|
||
- 例如:`OrderType` 不包含 `ml_order_items` 字段
|
||
- 强制转换后会丢失嵌套数据
|
||
|
||
2. 解决方案:使用 `any` 或 `Record<string, any>` 保留完整数据
|
||
```typescript
|
||
// 错误:强制转换丢失嵌套数据
|
||
const order: OrderType = {
|
||
id: o.getString('id') ?? '',
|
||
// ... 其他字段
|
||
} as OrderType
|
||
// ml_order_items 数据丢失!
|
||
|
||
// 正确:保留嵌套数据
|
||
const orderItem: Record<string, any> = {
|
||
id: o.getString('id') ?? '',
|
||
// ... 其他字段
|
||
ml_order_items: o.get('ml_order_items') // 保留嵌套数据
|
||
}
|
||
```
|
||
|
||
3. 最佳实践
|
||
- 如果需要访问嵌套数据,不要使用不包含这些字段的类型
|
||
- 使用 `Record<string, any>` 或 `any` 保留完整数据结构
|
||
- 在访问嵌套数据时进行类型转换
|
||
|
||
================================================================================
|
||
一百、Supabase 查询中整数字段处理(重要)
|
||
================================================================================
|
||
|
||
1. 问题:Supabase 查询时整数字段导致类型转换异常
|
||
- 错误信息:`java.lang.Integer cannot be cast to java.lang.String`
|
||
- 原因:UTS Android 中 Supabase 查询的 `.eq()`、`.in()` 等方法对整数字段需要使用字符串形式
|
||
|
||
2. 解决方案:整数字段使用字符串形式
|
||
```typescript
|
||
// 错误:直接使用数字
|
||
.eq('target_type', 1)
|
||
.eq('status', 1)
|
||
.in('category_id', [1, 2, 3])
|
||
|
||
// 正确:使用字符串形式
|
||
.eq('target_type', '1')
|
||
.eq('status', '1')
|
||
.in('category_id', ['1', '2', '3'])
|
||
```
|
||
|
||
3. insert 语句中同样需要使用字符串
|
||
```typescript
|
||
// 错误:直接使用数字
|
||
.insert({
|
||
target_type: 1,
|
||
status: 1
|
||
})
|
||
|
||
// 正确:使用字符串形式
|
||
.insert({
|
||
target_type: '1',
|
||
status: '1'
|
||
})
|
||
```
|
||
|
||
4. 常见需要处理的整数字段
|
||
- `target_type` - 收藏类型(1:商品, 2:店铺)
|
||
- `status` - 各种状态字段
|
||
- `order_status` - 订单状态
|
||
- `payment_status` - 支付状态
|
||
- `shipping_status` - 发货状态
|
||
|
||
5. 最佳实践
|
||
- 所有 Supabase 查询中的整数值都使用字符串形式
|
||
- 使用 UTSJSONObject.set() 设置整数字段时也使用字符串
|
||
- 从数据库读取的整数值需要安全转换后再使用
|
||
|
||
================================================================================
|
||
一百零一、数据库字段类型安全转换(重要)
|
||
================================================================================
|
||
|
||
1. 问题:数据库返回的字段类型可能与预期不符
|
||
- 错误信息:`java.lang.Integer cannot be cast to java.lang.String`
|
||
- 原因:UUID 字段可能返回为 Integer,需要安全转换
|
||
|
||
2. 解决方案:使用类型检查后转换
|
||
```typescript
|
||
// 安全获取字符串类型的 ID
|
||
const targetIdRaw = itemObj.get('target_id')
|
||
let targetId = ''
|
||
if (targetIdRaw != null) {
|
||
if (typeof targetIdRaw === 'string') {
|
||
targetId = targetIdRaw as string
|
||
} else if (typeof targetIdRaw === 'number') {
|
||
targetId = (targetIdRaw as number).toString()
|
||
}
|
||
}
|
||
```
|
||
|
||
3. 通用辅助函数
|
||
```typescript
|
||
// 安全获取字符串值
|
||
function safeGetString(obj: UTSJSONObject, key: string): string {
|
||
const val = obj.get(key)
|
||
if (val == null) return ''
|
||
if (typeof val === 'string') return val as string
|
||
if (typeof val === 'number') return (val as number).toString()
|
||
return ''
|
||
}
|
||
|
||
// 安全获取数值
|
||
function safeGetNumber(obj: UTSJSONObject, key: string): number {
|
||
const val = obj.get(key)
|
||
if (val == null) return 0
|
||
if (typeof val === 'number') return val as number
|
||
if (typeof val === 'string') return parseFloat(val as string) ?? 0
|
||
return 0
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 所有从数据库获取的字段都进行类型检查
|
||
- 不要假设字段类型,使用 typeof 检查
|
||
- 提供默认值防止空指针异常
|
||
|
||
================================================================================
|
||
一百零二、JSON.parse() 返回值处理(重要)
|
||
================================================================================
|
||
|
||
1. 问题:JSON.parse() 返回 Any? 类型
|
||
- 错误信息:`参数类型不匹配:实际类型为 'Any?',预期类型为 'Any'`
|
||
- 原因:JSON.parse() 返回的是可空类型,不能直接传给需要 Any 类型的函数
|
||
|
||
2. 解决方案:检查 null 后再使用
|
||
```typescript
|
||
// 错误:直接传递 JSON.parse() 结果
|
||
const parsed = JSON.parse(specs)
|
||
return formatSpecs(parsed) // 错误!parsed 是 Any? 类型
|
||
|
||
// 正确:先检查 null
|
||
const parsed = JSON.parse(specs)
|
||
if (parsed != null) {
|
||
return formatSpecs(parsed)
|
||
}
|
||
return ''
|
||
```
|
||
|
||
3. 完整示例
|
||
```typescript
|
||
const getSpecText = (specs: any): string => {
|
||
if (specs == null) return ''
|
||
if (typeof specs === 'string') {
|
||
if (specs == '') return ''
|
||
try {
|
||
const parsed = JSON.parse(specs)
|
||
if (parsed != null) {
|
||
return formatSpecs(parsed)
|
||
}
|
||
return specs
|
||
} catch (e) {
|
||
return specs
|
||
}
|
||
}
|
||
return formatSpecs(specs)
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- JSON.parse() 返回值必须检查 null
|
||
- 使用 try-catch 包裹 JSON.parse() 防止解析异常
|
||
- 提供默认值或后备方案
|
||
|
||
================================================================================
|
||
一百零三、响应式对象不能直接转换为 Map/Record(重要)
|
||
================================================================================
|
||
|
||
1. 问题:ref 对象的值不能直接转换为 Record<string, any>
|
||
- 错误信息:`AddressTypeReactiveObject cannot be cast to Map`
|
||
- 原因:Vue 的响应式对象在 UTS Android 中是特殊的代理类型,不能直接转换
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const deliveryAddress = ref<AddressType | null>(null)
|
||
|
||
const getFullAddress = (addr: any): string => {
|
||
if (addr == null) return ''
|
||
if (typeof addr === 'string') return addr
|
||
|
||
// 错误:响应式对象不能直接转换为 Record
|
||
const addrObj = addr as Record<string, any>
|
||
return addrObj['province'] as string // 运行时错误
|
||
}
|
||
|
||
// 模板中调用
|
||
{{ getFullAddress(deliveryAddress as any) }}
|
||
```
|
||
|
||
3. 正确解决方案:使用 JSON.parse(JSON.stringify()) 转换
|
||
```typescript
|
||
const getFullAddress = (addr: any): string => {
|
||
if (addr == null) return ''
|
||
if (typeof addr === 'string') return addr
|
||
|
||
try {
|
||
// 正确:先序列化再解析为 UTSJSONObject
|
||
const addrObj = JSON.parse(JSON.stringify(addr)) as UTSJSONObject
|
||
|
||
const addressField = addrObj.getString('address')
|
||
if (addressField != null && addressField != '') return addressField
|
||
|
||
const province = addrObj.getString('province') ?? ''
|
||
const city = addrObj.getString('city') ?? ''
|
||
const district = addrObj.getString('district') ?? ''
|
||
const detail = addrObj.getString('detail') ?? ''
|
||
return province + city + district + detail
|
||
} catch (e) {
|
||
console.error('[getFullAddress] 解析地址失败:', e)
|
||
return ''
|
||
}
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 响应式对象传递给函数时,必须使用 JSON.parse(JSON.stringify()) 转换
|
||
- 转换后使用 UTSJSONObject 的 getString()、getNumber() 方法访问属性
|
||
- 使用 try-catch 包裹防止转换异常
|
||
- 对于简单场景,可以直接在模板中访问响应式对象的属性
|
||
|
||
================================================================================
|
||
一百零四、数据库关联查询可能返回 null(重要)
|
||
================================================================================
|
||
|
||
1. 问题:Supabase 关联查询可能返回 null
|
||
- 症状:`ml_order_items: null` 即使数据库有关联数据
|
||
- 原因:
|
||
1. 关联表中确实没有数据
|
||
2. 外键关系配置不正确
|
||
3. 查询语法问题
|
||
|
||
2. 检查步骤
|
||
```typescript
|
||
// 1. 检查原始数据
|
||
console.log('[loadOrderDetail] 订单商品数据:', itemsRaw)
|
||
|
||
// 2. 正确处理 null 情况
|
||
if (itemsRaw != null && Array.isArray(itemsRaw)) {
|
||
// 处理数据
|
||
} else {
|
||
console.warn('[loadOrderDetail] 订单商品数据为空')
|
||
}
|
||
```
|
||
|
||
3. 确保 Supabase 查询正确
|
||
```typescript
|
||
// 正确的关联查询语法
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.select(`
|
||
*,
|
||
ml_order_items (
|
||
id,
|
||
product_id,
|
||
product_name,
|
||
price,
|
||
quantity,
|
||
image_url,
|
||
specifications
|
||
)
|
||
`)
|
||
.eq('id', orderId)
|
||
.limit(1)
|
||
.execute()
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 始终检查关联数据是否为 null
|
||
- 添加日志输出原始数据便于调试
|
||
- 给用户友好的提示(如"暂无商品数据")
|
||
|
||
================================================================================
|
||
一百零五、as UTSJSONObject 不会添加方法(重要)
|
||
================================================================================
|
||
|
||
1. 问题:直接使用 `as UTSJSONObject` 类型断言不会让对象获得 getString/getNumber 等方法
|
||
- 错误信息:`UTSJSONObject cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:类型断言只是编译时行为,不会改变运行时对象的方法
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const rawItems = orderData.items as any[]
|
||
|
||
for(let i = 0; i < rawItems.length; i++) {
|
||
// 错误:直接 as UTSJSONObject 不会添加方法
|
||
const item = rawItems[i] as UTSJSONObject
|
||
|
||
// 运行时错误:getString is not a function
|
||
const productId = item.getString('product_id')
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:使用 JSON.parse(JSON.stringify()) 进行真正的转换
|
||
```typescript
|
||
const rawItems = orderData.items as any[]
|
||
|
||
for(let i = 0; i < rawItems.length; i++) {
|
||
// 正确:使用 JSON.parse(JSON.stringify()) 转换
|
||
const item = JSON.parse(JSON.stringify(rawItems[i])) as UTSJSONObject
|
||
|
||
// 正确工作
|
||
const productId = item.getString('product_id')
|
||
}
|
||
```
|
||
|
||
4. 常见场景
|
||
- 从 Supabase 查询返回的数组元素
|
||
- 从 API 响应获取的数据
|
||
- 从页面传递的参数对象
|
||
- 嵌套在父对象中的子对象
|
||
|
||
5. 最佳实践
|
||
- 所有需要调用 getString/getNumber/getArray 方法的地方,必须先用 JSON.parse(JSON.stringify()) 转换
|
||
- 不要依赖 `as UTSJSONObject` 类型断言来添加方法
|
||
- 对于复杂对象,统一使用转换函数处理
|
||
|
||
================================================================================
|
||
一百零六、函数间传递对象需要序列化(重要)
|
||
================================================================================
|
||
|
||
1. 问题:在函数间传递对象或数组时,可能遇到类型转换错误
|
||
- 错误信息:`UTSJSONObject cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:Vue 响应式对象、UTSJSONObject 等特殊类型在传递过程中可能无法正确序列化
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
// checkout.uvue 中构建商品数组
|
||
const items: any[] = []
|
||
for (let j = 0; j < group.items.length; j++) {
|
||
const item = group.items[j] // Vue 响应式对象
|
||
items.push({
|
||
id: item.id,
|
||
product_id: item.product_id,
|
||
// ...
|
||
})
|
||
}
|
||
|
||
// 传递给服务层
|
||
await supabaseService.createOrder({
|
||
items: items // 可能包含响应式代理对象
|
||
})
|
||
|
||
// 服务层处理
|
||
const rawItems = orderData.items as any[]
|
||
for(let i = 0; i < rawItems.length; i++) {
|
||
const item = JSON.parse(JSON.stringify(rawItems[i])) as UTSJSONObject
|
||
// 仍然可能失败,因为 rawItems[i] 可能是特殊对象
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:在传递前完全序列化
|
||
```typescript
|
||
// 在服务层接收参数时,先将整个数组序列化
|
||
const plainItems: any[] = []
|
||
for(let k = 0; k < shopItems.length; k++) {
|
||
const plainItem = JSON.parse(JSON.stringify(shopItems[k]))
|
||
plainItems.push(plainItem)
|
||
}
|
||
|
||
// 然后传递给内部函数
|
||
const orderId = await this.createOrder({
|
||
items: plainItems // 现在是普通对象数组
|
||
})
|
||
|
||
// 在 createOrder 中处理
|
||
for(let i = 0; i < rawItems.length; i++) {
|
||
const itemStr = JSON.stringify(rawItems[i])
|
||
const item = JSON.parse(itemStr) as UTSJSONObject
|
||
// 现在可以正确使用 getString/getNumber
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 在函数边界处,将复杂对象序列化为普通对象
|
||
- 使用 `JSON.parse(JSON.stringify(obj))` 确保对象是普通 JavaScript 对象
|
||
- 添加日志输出对象类型便于调试
|
||
- 对于嵌套对象,每一层都需要序列化
|
||
|
||
================================================================================
|
||
一百零七、CSS 属性值限制(重要)
|
||
================================================================================
|
||
|
||
1. 问题:UTS Android 不支持某些 CSS 属性值
|
||
- `gap` 属性不支持
|
||
- `calc()` 函数不支持
|
||
- `font-weight: 800` 等数值不支持(只支持 normal/bold/400/700)
|
||
- `env()` 和 `constant()` 函数不支持
|
||
|
||
2. 错误示例
|
||
```css
|
||
/* 错误:gap 不支持 */
|
||
.container {
|
||
display: flex;
|
||
gap: 20px;
|
||
}
|
||
|
||
/* 错误:calc() 不支持 */
|
||
.card {
|
||
width: calc(50% - 10px);
|
||
}
|
||
|
||
/* 错误:font-weight 数值不支持 */
|
||
.title {
|
||
font-weight: 800;
|
||
}
|
||
|
||
/* 错误:env()/constant() 不支持 */
|
||
.bottom-bar {
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案
|
||
```css
|
||
/* 正确:使用 margin 替代 gap */
|
||
.container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
.item {
|
||
margin: 10px;
|
||
}
|
||
|
||
/* 正确:使用百分比和 margin 替代 calc() */
|
||
.card {
|
||
width: 48%;
|
||
margin: 0 1%;
|
||
}
|
||
|
||
/* 正确:使用 bold 或 700 */
|
||
.title {
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 正确:使用固定像素值 */
|
||
.bottom-bar {
|
||
padding-bottom: 30px;
|
||
}
|
||
```
|
||
|
||
4. font-weight 支持的值
|
||
- `normal` (等同于 400)
|
||
- `bold` (等同于 700)
|
||
- `400`
|
||
- `700`
|
||
|
||
5. 最佳实践
|
||
- 避免使用 `gap`,改用 `margin`
|
||
- 避免使用 `calc()`,改用百分比和 margin 组合
|
||
- font-weight 只使用 `normal`、`bold`、`400`、`700`
|
||
- 安全区域适配使用固定像素值(如 30px)
|
||
|
||
6. 其他不支持的 CSS 属性
|
||
- `overflow-wrap` 不支持
|
||
- `cursor` 不支持
|
||
- `max-width` 不支持百分比值,只支持 `number`|`pixel`
|
||
|
||
================================================================================
|
||
一百零八、模板中 || 运算符限制(重要)
|
||
================================================================================
|
||
|
||
1. 问题:模板中使用 `||` 运算符可能导致编译错误
|
||
- 错误信息:`Conditional statements must use boolean types`
|
||
- 原因:UTS 模板中 `||` 左边必须是 boolean 类型
|
||
|
||
2. 错误示例
|
||
```html
|
||
<!-- 错误:|| 左边不是 boolean 类型 -->
|
||
<text>{{ order.shop_name || '自营店铺' }}</text>
|
||
<text>{{ item.name || '默认名称' }}</text>
|
||
```
|
||
|
||
3. 正确解决方案:使用三元表达式
|
||
```html
|
||
<!-- 正确:使用三元表达式 -->
|
||
<text>{{ order.shop_name != null && order.shop_name != '' ? order.shop_name : '自营店铺' }}</text>
|
||
<text>{{ item.name != null && item.name != '' ? item.name : '默认名称' }}</text>
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 模板中避免使用 `||` 运算符处理默认值
|
||
- 使用三元表达式:`condition ? value : default`
|
||
- 对于字符串,使用 `!= null && != ''` 判断
|
||
|
||
================================================================================
|
||
一百零九、JSON.stringify 返回 Any? 类型(重要)
|
||
================================================================================
|
||
|
||
1. 问题:JSON.stringify() 返回 Any? 类型
|
||
- 错误信息:`参数类型不匹配:实际类型为 'Any?',预期类型为 'Any'`
|
||
- 原因:JSON.stringify() 返回可空类型,不能直接使用
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const plainItems: any[] = []
|
||
for(let k = 0; k < shopItems.length; k++) {
|
||
const plainItem = JSON.parse(JSON.stringify(shopItems[k]))
|
||
plainItems.push(plainItem) // 错误!plainItem 是 Any? 类型
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:检查 null 后再使用
|
||
```typescript
|
||
const plainItems: any[] = []
|
||
for(let k = 0; k < shopItems.length; k++) {
|
||
const plainItemRaw = JSON.parse(JSON.stringify(shopItems[k]))
|
||
if (plainItemRaw != null) {
|
||
plainItems.push(plainItemRaw as any)
|
||
}
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- JSON.parse() 返回值必须检查 null
|
||
- 使用 `as any` 或其他具体类型进行类型转换
|
||
|
||
================================================================================
|
||
一百一十、类型定义必须包含所有字段(重要)
|
||
================================================================================
|
||
|
||
1. 问题:创建类型实例时缺少必填字段
|
||
- 错误信息:`No value passed for parameter 'xxx'`
|
||
- 原因:类型定义中的字段都是必填的,必须提供所有字段值
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
type OrderItem = {
|
||
id: string,
|
||
order_no: string,
|
||
merchant_id: string, // 必填字段
|
||
shop_name: string, // 必填字段
|
||
products: OrderProduct[]
|
||
}
|
||
|
||
// 错误:缺少 merchant_id 和 shop_name
|
||
typedOrders.push({
|
||
id: mo['id'] as string,
|
||
order_no: mo['order_no'] as string,
|
||
products: mo['products'] as OrderProduct[]
|
||
})
|
||
```
|
||
|
||
3. 正确解决方案:提供所有必填字段
|
||
```typescript
|
||
typedOrders.push({
|
||
id: mo['id'] as string,
|
||
order_no: mo['order_no'] as string,
|
||
merchant_id: (mo['merchant_id'] ?? '') as string,
|
||
shop_name: (mo['shop_name'] ?? '自营店铺') as string,
|
||
products: mo['products'] as OrderProduct[]
|
||
})
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 检查类型定义,确认所有字段
|
||
- 使用 `??` 提供默认值
|
||
- 对于可能为空的字段,在类型定义中使用 `string | null` 或提供默认值
|
||
|
||
================================================================================
|
||
一百一十一、UTSJSONObject.get() 返回值需要类型转换(重要)
|
||
================================================================================
|
||
|
||
1. 问题:UTSJSONObject.get() 返回 Any? 类型,不能直接传给需要具体类型的函数
|
||
- 错误信息:`UTSJSONObject cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:get() 返回的是 Any? 类型,需要先转换为具体类型
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const item = JSON.parse(itemStr) as UTSJSONObject
|
||
|
||
// 错误:get() 返回 Any?,不能直接传给 set()
|
||
itemJson.set('product_id', item.get('product_id'))
|
||
itemJson.set('product_name', item.get('product_name') ?? '')
|
||
```
|
||
|
||
3. 正确解决方案:先转换为具体类型
|
||
```typescript
|
||
const itemParsed = JSON.parse(itemStr)
|
||
if (itemParsed == null) {
|
||
console.error('解析失败')
|
||
continue
|
||
}
|
||
const item = itemParsed as UTSJSONObject
|
||
|
||
// 正确:先获取值,再转换类型
|
||
let pId = item.get('product_id')
|
||
if (pId == null) {
|
||
pId = item.get('id')
|
||
}
|
||
const productId = (pId ?? '') as string
|
||
itemJson.set('product_id', productId)
|
||
|
||
const productName = (item.get('product_name') ?? '') as string
|
||
itemJson.set('product_name', productName)
|
||
```
|
||
|
||
4. 最佳实践
|
||
- JSON.parse() 返回值先检查 null,再 as UTSJSONObject
|
||
- get() 返回值使用 `??` 提供默认值,再 `as string` 或其他类型
|
||
- 对于可选字段,始终提供默认值
|
||
|
||
================================================================================
|
||
一百一十二、onBackPress 生命周期问题(重要)
|
||
================================================================================
|
||
|
||
1. 问题:onBackPress 在 <script setup> 中可能导致编译失败
|
||
- 错误信息:`Failed to fetch dynamically imported module`
|
||
- 原因:UTS 中 onBackPress 的调用方式可能与标准 Vue 不同
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
<script setup lang="uts">
|
||
import { onBackPress } from '@dcloudio/uni-app'
|
||
|
||
// 错误:在 setup 顶层直接调用可能导致问题
|
||
onBackPress((options) => {
|
||
// ...
|
||
return false
|
||
})
|
||
</script>
|
||
```
|
||
|
||
3. 解决方案
|
||
- 移除 onBackPress 钩子,或使用其他方式处理返回事件
|
||
- 如果需要拦截返回,考虑在页面跳转逻辑中处理
|
||
|
||
4. 最佳实践
|
||
- 避免在 <script setup> 中使用 onBackPress
|
||
- 使用页面跳转参数或状态管理处理导航逻辑
|
||
|
||
================================================================================
|
||
一百一十三、UTSJSONObject 不能直接用于 Supabase insert(重要)
|
||
================================================================================
|
||
|
||
1. 问题:UTSJSONObject 不能直接传给 Supabase 的 insert 方法
|
||
- 错误信息:`UTSJSONObject cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:Supabase insert 需要普通 JavaScript 对象,不是 UTSJSONObject
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const itemJson = new UTSJSONObject()
|
||
itemJson.set('order_id', orderId)
|
||
itemJson.set('product_id', productId)
|
||
// ...
|
||
|
||
// 错误:直接传 UTSJSONObject
|
||
const response = await supa
|
||
.from('ml_order_items')
|
||
.insert(itemJson) // 错误!
|
||
.execute()
|
||
```
|
||
|
||
3. 正确解决方案:转换为普通对象
|
||
```typescript
|
||
const itemJson = new UTSJSONObject()
|
||
itemJson.set('order_id', orderId)
|
||
itemJson.set('product_id', productId)
|
||
// ...
|
||
|
||
// 正确:先转换为普通对象
|
||
const itemObjStr = JSON.stringify(itemJson)
|
||
const itemObj = JSON.parse(itemObjStr) as Record<string, any>
|
||
|
||
const response = await supa
|
||
.from('ml_order_items')
|
||
.insert(itemObj) // 正确
|
||
.execute()
|
||
```
|
||
|
||
4. 最佳实践
|
||
- Supabase insert/update 操作必须使用普通对象
|
||
- 使用 JSON.stringify + JSON.parse 转换 UTSJSONObject
|
||
- 转换后使用 `as Record<string, any>` 类型
|
||
- **重要**:JSON.parse 返回 Any?,必须先检查 null 再转换
|
||
|
||
5. 完整示例
|
||
```typescript
|
||
// 错误:JSON.parse 返回 Any?,不能直接 as
|
||
const itemObj = JSON.parse(itemObjStr) as Record<string, any>
|
||
|
||
// 正确:先检查 null
|
||
const itemObjParsed = JSON.parse(itemObjStr)
|
||
if (itemObjParsed == null) {
|
||
console.error('转换失败')
|
||
continue
|
||
}
|
||
const itemObj = itemObjParsed as Record<string, any>
|
||
```
|
||
|
||
================================================================================
|
||
一百一十四、数组元素不能直接 as Record<string, any>(重要)
|
||
================================================================================
|
||
|
||
1. 问题:从 Supabase 查询返回的数组元素不能直接转换为 Record<string, any>
|
||
- 错误信息:`UTSJSONObject cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:数组元素是特殊的 UTS 对象,不能直接类型断言
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const queryData = queryResponse.data as any
|
||
if (Array.isArray(queryData) && queryData.length > 0) {
|
||
// 错误:不能直接 as Record<string, any>
|
||
const firstItem = queryData[0] as Record<string, any>
|
||
const orderId = firstItem['id'] as string
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:使用 JSON 序列化转换
|
||
```typescript
|
||
const queryData = queryResponse.data as any
|
||
if (Array.isArray(queryData) && queryData.length > 0) {
|
||
const firstItemRaw = queryData[0]
|
||
|
||
// 正确:使用 JSON 序列化转换
|
||
const firstItemStr = JSON.stringify(firstItemRaw)
|
||
const firstItemParsed = JSON.parse(firstItemStr)
|
||
if (firstItemParsed == null) {
|
||
console.error('解析失败')
|
||
return null
|
||
}
|
||
const firstItem = firstItemParsed as UTSJSONObject
|
||
const orderId = (firstItem.getString('id') ?? '') as string
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 从 Supabase 返回的数组元素必须先序列化再访问
|
||
- 使用 UTSJSONObject 的 getString/getNumber 方法访问属性
|
||
- 始终检查 JSON.parse 返回值是否为 null
|
||
|
||
================================================================================
|
||
一百一十五、currentPage.options 不能直接类型转换(重要)
|
||
================================================================================
|
||
|
||
1. 问题:currentPage.options 不能直接 as Record<string, any>
|
||
- 错误信息:`OnLoadOptions cannot be cast to Map`
|
||
- 原因:页面选项是特殊的 UTS 类型,不能直接转换
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
import { onMounted } from 'vue'
|
||
|
||
onMounted(() => {
|
||
const pages = getCurrentPages()
|
||
const currentPage = pages[pages.length - 1]
|
||
// 错误:不能直接 as Record<string, any>
|
||
const options = currentPage.options as Record<string, any>
|
||
const orderId = options['orderId']
|
||
})
|
||
```
|
||
|
||
3. 正确解决方案:使用 onLoad 生命周期获取页面参数
|
||
```typescript
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
|
||
// 正确:使用 onLoad 获取页面参数
|
||
onLoad((options) => {
|
||
if (options != null) {
|
||
const orderIdValue = options['orderId']
|
||
if (orderIdValue != null) {
|
||
orderId.value = orderIdValue as string
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 使用 onLoad 生命周期获取页面参数,而不是 getCurrentPages()
|
||
- onLoad 的 options 参数可以直接通过 key 访问
|
||
- 始终检查 options 和参数值是否为 null
|
||
|
||
================================================================================
|
||
一百一十六、uni.getStorageSync 返回值不能直接类型转换(重要)
|
||
================================================================================
|
||
|
||
1. 问题:uni.getStorageSync 返回的存储数据不能直接 as Record<string, any>
|
||
- 错误信息:`UTSJSONObject cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:存储数据可能是特殊的 UTS 类型,不能直接转换
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const userStore = uni.getStorageSync('userInfo')
|
||
if (userStore != null) {
|
||
// 错误:不能直接 as Record<string, any>
|
||
const userObj = userStore as Record<string, any>
|
||
const id = userObj['id']
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:使用 JSON 序列化转换
|
||
```typescript
|
||
const userStore = uni.getStorageSync('userInfo')
|
||
if (userStore != null) {
|
||
// 正确:使用 JSON 序列化转换
|
||
const userStr = JSON.stringify(userStore)
|
||
const userParsed = JSON.parse(userStr)
|
||
if (userParsed != null) {
|
||
const userObj = userParsed as UTSJSONObject
|
||
const id = userObj.getString('id')
|
||
if (id != null) {
|
||
return id
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- uni.getStorageSync 返回值必须先序列化再访问
|
||
- 使用 UTSJSONObject 的 getString/getNumber 方法访问属性
|
||
- 始终检查 JSON.parse 返回值是否为 null
|
||
|
||
================================================================================
|
||
一百一十七、JSON.parse 空字符串会报错(重要)
|
||
================================================================================
|
||
|
||
1. 问题:JSON.parse 空字符串会抛出异常
|
||
- 错误信息:`JSON.parse error: input text is empty`
|
||
- 原因:空字符串不是有效的 JSON
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const ordersStr = uni.getStorageSync('orders')
|
||
if (ordersStr != null) {
|
||
// 错误:ordersStr 可能是空字符串 ''
|
||
const parsed = JSON.parse(ordersStr as string)
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:检查空字符串
|
||
```typescript
|
||
const ordersStr = uni.getStorageSync('orders')
|
||
if (ordersStr != null && ordersStr !== '') {
|
||
const parsed = JSON.parse(ordersStr as string)
|
||
// ...
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- JSON.parse 前检查字符串是否为空
|
||
- 使用 `!= null && !== ''` 双重检查
|
||
- uni.getStorageSync 可能返回空字符串
|
||
|
||
================================================================================
|
||
一百一十八、自定义类型不能转换为 Record<string, any>(重要)
|
||
================================================================================
|
||
|
||
1. 问题:自定义 type 类型不能直接转换为 Record<string, any>
|
||
- 错误信息:`OrderItem cannot be cast to io.dcloud.uts.Map`
|
||
- 原因:UTS 中的自定义类型是独立的类,不是 Map
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
type OrderItem = {
|
||
id: string,
|
||
order_no: string,
|
||
create_time: string
|
||
}
|
||
|
||
const orders: OrderItem[] = [...]
|
||
|
||
// 错误:尝试将 OrderItem 转换为 Record
|
||
orders.sort((a: any, b: any) => {
|
||
const aObj = a as Record<string, any> // 错误!
|
||
const bObj = b as Record<string, any> // 错误!
|
||
return new Date(aObj['create_time']).getTime() - new Date(bObj['create_time']).getTime()
|
||
})
|
||
```
|
||
|
||
3. 正确解决方案:直接使用类型属性
|
||
```typescript
|
||
type OrderItem = {
|
||
id: string,
|
||
order_no: string,
|
||
create_time: string
|
||
}
|
||
|
||
const orders: OrderItem[] = [...]
|
||
|
||
// 正确:直接使用 OrderItem 类型访问属性
|
||
orders.sort((a: OrderItem, b: OrderItem) => {
|
||
return new Date(a.create_time).getTime() - new Date(b.create_time).getTime()
|
||
})
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 自定义类型数组排序时,直接使用类型声明参数
|
||
- 不要尝试将自定义类型转换为 Record<string, any>
|
||
- 使用点号访问属性,而不是方括号
|
||
|
||
================================================================================
|
||
一百一十九、函数参数类型必须精确匹配(重要)
|
||
================================================================================
|
||
|
||
1. 问题:函数参数类型必须与传入的类型精确匹配
|
||
- 错误信息:`参数类型不匹配:实际类型为 'UTSArray<OrderItem>',预期类型为 'UTSArray<Any>'`
|
||
- 原因:UTS 是强类型语言,不支持隐式类型转换
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
type OrderItem = {
|
||
id: string,
|
||
status: number
|
||
}
|
||
|
||
// 错误:参数声明为 any[]
|
||
const updateTabsCounts = (allOrders: any[]) => {
|
||
const countPending = allOrders.filter((o: any) => {
|
||
const item = o as OrderItem // 多余的转换
|
||
return item.status === 1
|
||
}).length
|
||
}
|
||
|
||
const orders: OrderItem[] = [...]
|
||
updateTabsCounts(orders) // 错误!类型不匹配
|
||
```
|
||
|
||
3. 正确解决方案:使用精确的类型声明
|
||
```typescript
|
||
type OrderItem = {
|
||
id: string,
|
||
status: number
|
||
}
|
||
|
||
// 正确:参数声明为 OrderItem[]
|
||
const updateTabsCounts = (allOrders: OrderItem[]) => {
|
||
const countPending = allOrders.filter((o: OrderItem) => o.status === 1).length
|
||
}
|
||
|
||
const orders: OrderItem[] = [...]
|
||
updateTabsCounts(orders) // 正确!类型匹配
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 函数参数类型必须与实际传入类型一致
|
||
- 避免使用 any[] 作为自定义类型数组的参数
|
||
- 在 filter/map/sort 等回调中直接使用具体类型
|
||
|
||
================================================================================
|
||
一百二十、Object.keys() 不支持 UTSJSONObject(重要)
|
||
================================================================================
|
||
|
||
1. 问题:Object.keys() 不能用于 UTSJSONObject
|
||
- 错误信息:`找不到名称"keys"`
|
||
- 原因:UTSJSONObject 有自己的 keys() 方法
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const obj = orderParsed as UTSJSONObject
|
||
// 错误:Object.keys() 不支持 UTSJSONObject
|
||
const keys = Object.keys(obj)
|
||
```
|
||
|
||
3. 正确解决方案:使用 UTSJSONObject.keys() 方法
|
||
```typescript
|
||
const obj = orderParsed as UTSJSONObject
|
||
// 正确:使用 UTSJSONObject 的 keys() 方法
|
||
const keys = obj.keys()
|
||
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const key = keys[i]
|
||
const value = obj.get(key)
|
||
console.log(key, value)
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- UTSJSONObject 使用 `obj.keys()` 获取键列表
|
||
- 使用 `obj.get(key)` 获取值
|
||
- 使用 `obj.getString(key)` / `obj.getNumber(key)` 获取特定类型的值
|
||
|
||
================================================================================
|
||
一百二十一、Supabase 关联查询语法(重要)
|
||
================================================================================
|
||
|
||
1. 问题:多行模板字符串可能导致关联查询失败
|
||
- 现象:关联表数据返回 null
|
||
- 原因:Supabase JavaScript 客户端处理多行字符串时可能有问题
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
// 可能失败的多行语法
|
||
.select(`
|
||
*,
|
||
ml_order_items (*)
|
||
`)
|
||
```
|
||
|
||
3. 正确解决方案:使用单行字符串
|
||
```typescript
|
||
// 正确:单行字符串
|
||
.select('*, ml_order_items(*)')
|
||
```
|
||
|
||
4. 最佳实践
|
||
- Supabase 查询使用单行字符串
|
||
- 关联查询格式:`'*, related_table(*)'`
|
||
- 多个关联:`'*, table1(*), table2(*)'`
|
||
|
||
================================================================================
|
||
一百二十二、UTSJSONObject.keys() 方法在 Android 端不可用(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`UTSJSONObject.keys()` 方法在 Android 端编译失败
|
||
- 错误信息:`找不到名称"keys"`
|
||
- 原因:Android 端 UTSJSONObject 可能没有 keys() 方法
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const specObj = objParsed as UTSJSONObject
|
||
// 错误:keys() 方法不可用
|
||
const keys = specObj.keys()
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const key = keys[i]
|
||
// ...
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:直接获取已知字段或使用 JSON 字符串处理
|
||
```typescript
|
||
const specObj = objParsed as UTSJSONObject
|
||
|
||
// 方案一:直接获取已知字段
|
||
const colorVal = specObj.getString('Color')
|
||
const sizeVal = specObj.getString('Size')
|
||
const defaultVal = specObj.getString('默认')
|
||
|
||
// 方案二:使用 JSON 字符串处理
|
||
const specObjStr = JSON.stringify(specObj)
|
||
const formatted = specObjStr.replace(/[{}"]/g, '').replace(/:/g, ': ').replace(/,/g, ' | ')
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 避免使用 `UTSJSONObject.keys()` 方法
|
||
- 如果知道字段名,直接使用 `getString()` 获取
|
||
- 如果需要遍历,使用 JSON 字符串处理
|
||
|
||
================================================================================
|
||
一百二十三、函数声明顺序必须正确(重要)
|
||
================================================================================
|
||
|
||
1. 问题:使用 `const` 声明的函数在调用前必须已定义
|
||
- 错误信息:`找不到名称"xxx"`
|
||
- 原因:`const` 箭头函数在定义前无法被调用
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
// 错误:formatSpecObj 在定义前被调用
|
||
const parseSpecText = (specs: any): string => {
|
||
return formatSpecObj(specs) // 错误!formatSpecObj 还未定义
|
||
}
|
||
|
||
const formatSpecObj = (obj: any): string => {
|
||
return '...'
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:使用 `function` 声明或调整顺序
|
||
```typescript
|
||
// 方案一:使用 function 声明(推荐)
|
||
function formatSpecObj(obj: any): string {
|
||
return '...'
|
||
}
|
||
|
||
function parseSpecText(specs: any): string {
|
||
return formatSpecObj(specs) // 正确!function 声明会提升
|
||
}
|
||
|
||
// 方案二:调整定义顺序
|
||
const formatSpecObj = (obj: any): string => {
|
||
return '...'
|
||
}
|
||
|
||
const parseSpecText = (specs: any): string => {
|
||
return formatSpecObj(specs) // 正确!formatSpecObj 已定义
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 优先使用 `function` 声明,避免顺序问题
|
||
- 如果使用 `const` 箭头函数,确保定义在被调用之前
|
||
|
||
================================================================================
|
||
一百二十四、String() 构造函数不支持任意类型(重要)
|
||
================================================================================
|
||
|
||
1. 问题:`String(value)` 不支持任意类型转换
|
||
- 错误信息:`None of the following candidates is applicable`
|
||
- 原因:UTS 中 String() 只接受特定类型
|
||
|
||
2. 错误示例
|
||
```typescript
|
||
const value: any = obj.get(key)
|
||
// 错误:String() 不接受 any 类型
|
||
const str = String(value)
|
||
```
|
||
|
||
3. 正确解决方案:使用类型判断
|
||
```typescript
|
||
const value: any = obj.get(key)
|
||
let valueStr = ''
|
||
if (typeof value === 'string') {
|
||
valueStr = value
|
||
} else if (typeof value === 'number') {
|
||
valueStr = value.toString()
|
||
} else {
|
||
valueStr = JSON.stringify(value)
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- 使用 `typeof` 判断类型后再转换
|
||
- 数字使用 `.toString()` 方法
|
||
- 对象使用 `JSON.stringify()`
|
||
|
||
================================================================================
|
||
一百二十五、scroll-view 必须设置确定高度(重要)
|
||
================================================================================
|
||
|
||
1. 问题:scroll-view 无法滚动
|
||
- 现象:页面内容超出屏幕但无法向下滚动
|
||
- 原因:scroll-view 没有确定的高度
|
||
|
||
2. 错误示例
|
||
```css
|
||
.orders-content {
|
||
flex: 1;
|
||
}
|
||
```
|
||
|
||
3. 正确解决方案:添加 height: 0 配合 flex: 1
|
||
```css
|
||
.orders-content {
|
||
flex: 1;
|
||
height: 0; /* 重要:配合 flex: 1 使用 */
|
||
}
|
||
```
|
||
|
||
4. 最佳实践
|
||
- scroll-view 必须有确定的高度
|
||
- 使用 `flex: 1; height: 0;` 组合
|
||
- 或者直接设置具体高度如 `height: 500px`
|
||
|
||
================================================================================
|
||
一百二十六、Supabase 多表关联查询(重要)
|
||
================================================================================
|
||
|
||
1. 问题:需要从关联表获取数据
|
||
- 场景:订单表需要获取商家店铺名称
|
||
- 前提:必须在 Supabase 中设置正确的外键关系
|
||
|
||
2. 外键设置要求
|
||
- 在 Supabase 控制台设置外键关系
|
||
- 例如:`ml_orders.merchant_id` -> `ml_merchants.id`
|
||
- 没有外键关系会导致 400 Bad Request 错误
|
||
|
||
3. 示例:订单关联商家和订单项
|
||
```typescript
|
||
// 关联查询多个表(需要先设置外键)
|
||
const response = await supa
|
||
.from('ml_orders')
|
||
.select('*, ml_order_items(*), ml_merchants(shop_name)')
|
||
.eq('user_id', userId)
|
||
.execute()
|
||
```
|
||
|
||
4. 如果没有外键关系的替代方案
|
||
```typescript
|
||
// 方案一:使用默认值
|
||
let shopName = '自营店铺'
|
||
if (merchantId != null && merchantId != '') {
|
||
shopName = '商家店铺'
|
||
}
|
||
|
||
// 方案二:单独查询商家表
|
||
const merchantResponse = await supa
|
||
.from('ml_merchants')
|
||
.select('shop_name')
|
||
.eq('id', merchantId)
|
||
.single()
|
||
.execute()
|
||
```
|
||
|
||
5. 最佳实践
|
||
- 关联查询前确保外键关系已设置
|
||
- 使用单行字符串格式:`'*, table1(*), table2(field1, field2)'`
|
||
- 如果关联查询失败,使用替代方案
|
||
|
||
================================================================================
|
||
一百二十七、Android 特有 UI 与交互规范(最新补充)
|
||
================================================================================
|
||
|
||
1. 图标兼容性(Emoji 风险)
|
||
- **现象**:在 Android 真机上直接使用 Emoji(如 🛒、❤️)可能导致显示不全、模糊或样式不统一。
|
||
- **规范**:所有功能图标**必须使用本地 PNG 图片**。
|
||
- **路径**:底部导航图标放在 `static/tabbar/`,业务小图标放在 `pages/mall/consumer/icons/`。
|
||
- **TabBar 特别强调**:`pages.json` 中的 `tabBar` 图标路径严禁使用 `.svg`,必须使用 `.png` 以确保安卓稳定性。
|
||
|
||
2. 布局高度计算修复
|
||
- **场景**:Chat 页面或带有固定底部的详情页。
|
||
- **规范**:`scroll-view` 的父级必须是 `flex: column`,且调用 `scroll-view` 时应配合 `flex: 1; height: 0;`。
|
||
- **双重滚动校准**:Android 初始渲染可能导致滚动不到底。
|
||
```uts
|
||
setTimeout(() => { this.scrollTop = 99999 }, 200); // 首次快速定位
|
||
setTimeout(() => { this.scrollTop = 99999 + Math.random() }, 500); // 二次精准校准
|
||
```
|
||
|
||
3. 空状态展示(去行业化文案)
|
||
- **规范**:在数据加载中或为空时,严禁使用行业特定干扰词(如“暂无药品”)。
|
||
- **方案**:统一使用“正在加载商品...”或“该分类下暂无商品”,并辅以 `loading-state` 动画,提升加载感知。
|
||
|
||
4. SKU 解析与展示规范
|
||
- **规范**:从数据库返回的 JSON 规格数据(如 `sku_spec_attr`)必须通过格式化函数转换为易读文本。
|
||
- **示例**:`[{"key":"规格","value":"10mg*7片"}]` -> `规格: 10mg*7片`。
|
||
- **技巧**:在 `uts` 中处理 `UTSJSONObject` 时,推荐使用 `getString("value")` 进行容错处理。
|
||
|
||
================================================================================
|
||
文档结束
|
||
================================================================================
|
||
|
||
---
|
||
|
||
*最后更新:2026-03-04*
|