-
一、Java 市场饱和下的就业困境,鸿蒙成救赎之光作为一名双非院校软件工程专业的学生,我曾深陷 Java 就业市场的 “内卷漩涡”。随着 Java 开发者数量持续激增,市场早已呈现饱和状态,即便掌握了基础的 Java 开发技能,在求职大军中也难以脱颖而出。投出的简历大多石沉大海,偶尔收到的面试邀约,也因缺乏差异化的技术亮点而屡屡碰壁。就在我对就业前景感到迷茫之际,鸿蒙系统的更新迭代让我看到了新的希望 —— 这款国产自主可控的操作系统,正以迅猛的势头搭建起全新的生态体系,而鸿蒙开发领域的人才缺口,恰好为我们这些渴望突破的学生开发者打开了一扇窗。鸿蒙时代的到来,不再是遥远的概念,而是软件工程师可触及的 “另一条赛道”。二、结缘鸿蒙激励计划,开启从零到一的开发征程鸿蒙开发者激励计划的推出,更是让我坚定了投身鸿蒙开发的决心。该计划以完善鸿蒙生态为核心目标,不仅鼓励更多开发者拥抱国产操作系统,更通过实实在在的激励机制,为开发者提供了成长的动力与支持。今年 8 月,我在开发者联盟看到激励计划的报名信息后,毫不犹豫地加入其中,正式踏上了鸿蒙开发的探索之路。起初,我完全是鸿蒙开发的 “小白”,连最基础的开发环境搭建、ArkUI 框架认知都一片空白。为了打好基础,我系统性地跟着网课老师学习鸿蒙入门知识,从系统架构、开发工具的使用,到 UI 组件的布局、事件处理的逻辑,每一个知识点都反复琢磨、反复实操。那段时间,除了日常的课程学习,其余时间几乎都投入到了鸿蒙开发的学习中,屏幕前的教学视频、堆满笔记的笔记本、不断报错又不断调试的代码,构成了我生活的主旋律。随着基础逐渐扎实,我开始尝试简单的页面开发。从一个按钮的点击事件实现,到一个列表的循环渲染,再到页面之间的跳转逻辑,每一个小小的突破都让我备受鼓舞。但开发之路从非坦途,适配问题、语法差异、逻辑BUG等一系列难题接踵而至。记得第一次尝试实现一个带下拉刷新功能的页面时,因对鸿蒙的组件生命周期理解不深,导致页面频繁闪退,反复调试了两天才找到问题根源。那些熬夜排查 bug 的夜晚,那些因思路卡顿而陷入的焦虑,如今回想起来,都是成长路上最珍贵的沉淀。三、聚焦用户痛点,敲定跨应用记账 APP 方向在具备一定开发能力后,我萌生了独立开发一款鸿蒙 APP 的想法,希望通过实际项目将所学知识融会贯通,也为简历增添亮眼的实践经历。最初,我计划开发一款简单的备忘录类 APP,但当我满怀期待地提交上架申请时,却收到了驳回通知 —— 应用市场中同类产品已高度同质化,缺乏创新点。正当我陷入方向迷茫时,一位有鸿蒙开发上架经验的前辈提醒我:“开发 APP 要找准用户未被满足的需求,避开红海赛道。” 这句话点醒了我。结合当下年轻人的消费现状,我发现一个普遍痛点:如今超前消费盛行,很多人缺乏记账意识,常常陷入 “月光” 甚至负债的困境;而市面上的支付工具(微信、支付宝等)仅能记录自身平台的账单,无法实现跨应用、全场景的消费与收入记录。基于这一需求缺口,我敲定了新的开发方向 —— 一款支持跨应用、全场景的记账 APP,命名为《嘉嘉记账》。m.1203ce.InFo/pacct/518998.sHtMlm.1203ce.InFo/pacct/046597.sHtMlm.1203ce.InFo/pacct/093089.sHtMlm.1203ce.InFo/pacct/921215.sHtMlm.1203ce.InFo/pacct/855334.sHtMlm.1203ce.InFo/pacct/628282.sHtMlm.1203ce.InFo/pacct/729997.sHtMlm.1203ce.InFo/pacct/207231.sHtMlm.1203ce.InFo/pacct/344924.sHtMlm.1203ce.InFo/pacct/898208.sHtMlm.1203ce.InFo/pacct/650839.sHtMlm.1203ce.InFo/pacct/977575.sHtMlm.1203ce.InFo/pacct/362683.sHtMlm.1203ce.InFo/pacct/048931.sHtMlm.1203ce.InFo/pacct/136041.sHtMlm.1203ce.InFo/pacct/629222.sHtMlm.1203ce.InFo/pacct/138822.sHtMlm.1203ce.InFo/pacct/803582.sHtMlm.1203ce.InFo/pacct/825504.sHtMlm.1203ce.InFo/pacct/792193.sHtMlm.1203ce.InFo/pacct/879616.sHtMlm.1203ce.InFo/pacct/251489.sHtMlm.1203ce.InFo/pacct/957785.sHtMlm.1203ce.InFo/pacct/570489.sHtMlm.1203ce.InFo/pacct/570489.sHtMlm.1203ce.InFo/pacct/395854.sHtMlm.1203ce.InFo/pacct/395854.sHtMlm.1203ce.InFo/pacct/395854.sHtMlm.1203ce.InFo/pacct/395854.sHtMlm.1203ce.InFo/pacct/460427.sHtMlm.1203ce.InFo/pacct/471964.sHtMlm.1203ce.InFo/pacct/480171.sHtMlm.1203ce.InFo/pacct/480171.sHtMlm.1203ce.InFo/pacct/480171.sHtMlm.1203ce.InFo/pacct/381683.sHtMlm.1203ce.InFo/pacct/381683.sHtMlm.1203ce.InFo/pacct/706442.sHtMlm.1203ce.InFo/pacct/706442.sHtMlm.1203ce.InFo/pacct/583315.sHtMlm.1203ce.InFo/pacct/839330.sHtMlm.1203ce.InFo/pacct/103178.sHtMlm.1203ce.InFo/pacct/474876.sHtMlm.1203ce.InFo/pacct/069379.sHtMlm.1203ce.InFo/pacct/779089.sHtMlm.1203ce.InFo/pacct/394901.sHtMlm.1203ce.InFo/pacct/635890.sHtMlm.1203ce.InFo/pacct/288153.sHtMlm.1203ce.InFo/pacct/749256.sHtMlm.1203ce.InFo/pacct/461499.sHtMlm.1203ce.InFo/pacct/687237.sHtMlm.1203ce.InFo/pacct/393404.sHtMlm.1203ce.InFo/pacct/328221.sHtMlm.1203ce.InFo/pacct/094914.sHtMlm.1203ce.InFo/pacct/838982.sHtMlm.1203ce.InFo/pacct/519831.sHtMlm.1203ce.InFo/pacct/870767.sHtMlm.1203ce.InFo/pacct/817476.sHtMlm.1203ce.InFo/pacct/898282.sHtMlm.1203ce.InFo/pacct/778997.sHtMlm.1203ce.InFo/pacct/004212.sHtMlm.1203ce.InFo/pacct/765042.sHtMlm.1203ce.InFo/pacct/277660.sHtMlm.1203ce.InFo/pacct/733342.sHtMlm.1203ce.InFo/pacct/547548.sHtMlm.1203ce.InFo/pacct/651986.sHtMlm.1203ce.InFo/pacct/756053.sHtMlm.1203ce.InFo/pacct/402793.sHtMlm.1203ce.InFo/pacct/200704.sHtMlm.1203ce.InFo/pacct/621701.sHtMlm.1203ce.InFo/pacct/528024.sHtMlm.1203ce.InFo/pacct/176923.sHtMlm.1203ce.InFo/pacct/801845.sHtMlm.1203ce.InFo/pacct/066635.sHtMlm.1203ce.InFo/pacct/173835.sHtMlm.1203ce.InFo/pacct/879149.sHtMlm.1203ce.InFo/pacct/974905.sHtMlm.1203ce.InFo/pacct/470591.sHtMlm.1203ce.InFo/pacct/627953.sHtMlm.1203ce.InFo/pacct/037205.sHtMlm.1203ce.InFo/pacct/389930.sHtMlm.1203ce.InFo/pacct/384291.sHtMlm.1203ce.InFo/pacct/600413.sHtMlm.1203ce.InFo/pacct/576010.sHtMlm.1203ce.InFo/pacct/304384.sHtMlm.1203ce.InFo/pacct/434253.sHtMlm.1203ce.InFo/pacct/778727.sHtMlm.1203ce.InFo/pacct/987248.sHtMlm.1203ce.InFo/pacct/400905.sHtMlm.1203ce.InFo/pacct/170421.sHtMlm.1203ce.InFo/pacct/222275.sHtMlm.1203ce.InFo/pacct/571231.sHtMlm.1203ce.InFo/pacct/771826.sHtMlm.1203ce.InFo/pacct/096797.sHtMlm.1203ce.InFo/pacct/423418.sHtMlm.1203ce.InFo/pacct/496626.sHtMlm.1203ce.InFo/pacct/433091.sHtMlm.1203ce.InFo/pacct/274979.sHtMlm.1203ce.InFo/pacct/798354.sHtMlm.1203ce.InFo/pacct/430109.sHtMlm.1203ce.InFo/pacct/879453.sHtMlm.1203ce.InFo/pacct/623041.sHtMlm.1203ce.InFo/pacct/533198.sHtMlm.1203ce.InFo/pacct/114264.sHtMlm.1203ce.InFo/pacct/259500.sHtMlm.1203ce.InFo/pacct/405002.sHtMlm.1203ce.InFo/pacct/682649.sHtMlm.1203ce.InFo/pacct/352382.sHtMlm.1203ce.InFo/pacct/622694.sHtMlm.1203ce.InFo/pacct/291522.sHtMlm.1203ce.InFo/pacct/823143.sHtMlm.1203ce.InFo/pacct/210726.sHtMlm.1203ce.InFo/pacct/620034.sHtMlm.1203ce.InFo/pacct/271473.sHtMlm.1203ce.InFo/pacct/086435.sHtMlm.1203ce.InFo/pacct/200147.sHtMlm.1203ce.InFo/pacct/171189.sHtMlm.1203ce.InFo/pacct/436142.sHtMlm.1203ce.InFo/pacct/498505.sHtMlm.1203ce.InFo/pacct/147164.sHtMlm.1203ce.InFo/pacct/867428.sHtMlm.1203ce.InFo/pacct/390496.sHtMlm.1203ce.InFo/pacct/469138.sHtMlm.1203ce.InFo/pacct/409739.sHtMlm.1203ce.InFo/pacct/092832.sHtMlm.1203ce.InFo/pacct/871557.sHtMlm.1203ce.InFo/pacct/288327.sHtMlm.1203ce.InFo/pacct/021541.sHtMlm.1203ce.InFo/pacct/444897.sHtMlm.1203ce.InFo/pacct/986389.sHtMlm.1203ce.InFo/pacct/122755.sHtMlm.1203ce.InFo/pacct/054388.sHtMlm.1203ce.InFo/pacct/166712.sHtMlm.1203ce.InFo/pacct/240063.sHtMlm.1203ce.InFo/pacct/193566.sHtMlm.1203ce.InFo/pacct/014950.sHtMlm.1203ce.InFo/pacct/560876.sHtMlm.1203ce.InFo/pacct/141549.sHtMlm.1203ce.InFo/pacct/682926.sHtMlm.1203ce.InFo/pacct/996045.sHtMlm.1203ce.InFo/pacct/077194.sHtMlm.1203ce.InFo/pacct/189340.sHtMlm.1203ce.InFo/pacct/657245.sHtMlm.1203ce.InFo/pacct/069486.sHtMlm.1203ce.InFo/pacct/796191.sHtMlm.1203ce.InFo/pacct/953033.sHtMlm.1203ce.InFo/pacct/103978.sHtMlm.1203ce.InFo/pacct/694848.sHtMlm.1203ce.InFo/pacct/404851.sHtMlm.1203ce.InFo/pacct/925982.sHtMlm.1203ce.InFo/pacct/933356.sHtMlm.1203ce.InFo/pacct/911859.sHtMlm.1203ce.InFo/pacct/541860.sHtMlm.1203ce.InFo/pacct/541860.sHtMlm.1203ce.InFo/pacct/541860.sHtMlm.1203ce.InFo/pacct/541860.sHtMlm.1203ce.InFo/pacct/541860.sHtMlm.1203ce.InFo/pacct/541860.sHtMlm.1203ce.InFo/pacct/174606.sHtMlm.1203ce.InFo/pacct/323717.sHtMlm.1203ce.InFo/pacct/030158.sHtMlm.1203ce.InFo/pacct/958804.sHtMlm.1203ce.InFo/pacct/118515.sHtMlm.1203ce.InFo/pacct/078693.sHtMlm.1203ce.InFo/pacct/645602.sHtMlm.1203ce.InFo/pacct/588745.sHtMlm.1203ce.InFo/pacct/177920.sHtMlm.1203ce.InFo/pacct/289766.sHtMlm.1203ce.InFo/pacct/523337.sHtMlm.1203ce.InFo/pacct/872491.sHtMlm.1203ce.InFo/pacct/379108.sHtMlm.1203ce.InFo/pacct/815069.sHtMlm.1203ce.InFo/pacct/594260.sHtMlm.1203ce.InFo/pacct/701526.sHtMlm.1203ce.InFo/pacct/989102.sHtMlm.1203ce.InFo/pacct/033394.sHtMlm.1203ce.InFo/pacct/213535.sHtMlm.1203ce.InFo/pacct/533056.sHtMlm.1203ce.InFo/pacct/098510.sHtMlm.1203ce.InFo/pacct/551964.sHtMlm.1203ce.InFo/pacct/898408.sHtMlm.1203ce.InFo/pacct/059693.sHtMlm.1203ce.InFo/pacct/134812.sHtMlm.1203ce.InFo/pacct/837227.sHtMlm.1203ce.InFo/pacct/514606.sHtMlm.1203ce.InFo/pacct/656971.sHtMlm.1203ce.InFo/pacct/655801.sHtMlm.1203ce.InFo/pacct/803492.sHtMlm.1203ce.InFo/pacct/099105.sHtMlm.1203ce.InFo/pacct/288607.sHtMlm.1203ce.InFo/pacct/873868.sHtMlm.1203ce.InFo/pacct/262812.sHtMlm.1203ce.InFo/pacct/737857.sHtMlm.1203ce.InFo/pacct/680117.sHtMlm.1203ce.InFo/pacct/766378.sHtMlm.1203ce.InFo/pacct/202941.sHtMlm.1203ce.InFo/pacct/624415.sHtMlm.1203ce.InFo/pacct/190127.sHtMlm.1203ce.InFo/pacct/269947.sHtMl这款 APP 的核心定位是 “一站式记账工具”,涵盖微信支付、支付宝、谷歌支付、银行卡刷卡、现金支付等全支付场景,同时细分消费类别(吃穿用度、加油旅游、日常通勤等)与收入类别(工资奖金、投资收益、兼职收入等),让用户能清晰掌握每一笔资金的来龙去脉,帮助养成合理消费、理性储蓄的习惯。
-
本群定期分享鸿蒙5.0以上开发相关技术,帮助提升产品日活互助群。
-
本人致力于物联网与智能家居领域的技术研究与产品验证,特别关注基于国产自主通信协议相关硬件的新一代智能终端开发。 目前没有“小熊派-鸿蒙·叔 (BearPi-HM Micro)”,全是理论的学习,不能进入到实践项目。更深刻的学习小熊派-鸿蒙系统,请求申请“小熊派-鸿蒙·叔 (BearPi-HM Micro)”,期望得到反馈。
-
11月28号的鸿蒙盛典大家都去吗?小编因为要去牛马所以不一定准时到,这次会议的主题是什么呀?听说还有刘德华 黄渤明星前来,鸿蒙真的是好起来了
-
一、技术选型与核心优势1. 技术栈说明开发语言:ArkTS(鸿蒙首选开发语言,声明式语法更高效)UI 框架:ArkUI(声明式 UI,支持组件化复用)核心 API:@State/@Link状态管理、animateTo动画、Column/Row弹性布局2. 为什么自定义折叠面板?系统原生Collapse组件扩展性差(无法自定义箭头、动画时长)自定义组件可适配多端(手机、平板、车机)支持复杂内容嵌套(列表、表单、图片)二、完整实现步骤(API 9+)Step 1:定义组件结构与状态// CustomCollapse.ets import { AnimateParam } from '@ohos/ui'; // 组件参数类型定义 interface CollapseProps { title: string; // 面板标题 defaultExpanded?: boolean; // 默认是否展开(默认false) content: () => void; // 面板内容(插槽) } @Component export struct CustomCollapse { // 接收外部参数 private props: CollapseProps; // 内部状态:控制展开/折叠 @State private isExpanded: boolean = false; // 初始化:设置默认展开状态 aboutToAppear() { this.isExpanded = this.props.defaultExpanded ?? false; } build() { Column() { // 1. 面板头部(点击触发折叠) Row() { Text(this.props.title) .fontSize(18) .fontWeight(FontWeight.Medium) .flexGrow(1); // 占满剩余空间 // 箭头图标(旋转动画) Image($r('app.media.ic_arrow_down')) .width(24) .height(24) .rotate({ angle: this.isExpanded ? 180 : 0, // 展开时旋转180° centerX: '50%', centerY: '50%' }) .transition({ type: TransitionType.ALL, duration: 300 }); // 过渡动画 } .padding(16) .backgroundColor('#f5f5f5') .onClick(() => { // 点击切换展开状态 this.isExpanded = !this.isExpanded; }); // 2. 面板内容(折叠/展开动画) if (this.isExpanded) { Column() { this.props.content(); // 插入外部传入的内容 } .padding(16) .backgroundColor('#ffffff') .animateTo({ duration: 300, // 动画时长 curve: Curve.EaseInOut // 缓动曲线 } as AnimateParam); } } .borderRadius(8) .margin(16) .shadow({ radius: 2, color: '#00000010' }); // 阴影效果 } } Step 2:组件使用示例// 页面中引用组件 import { CustomCollapse } from './CustomCollapse'; @Entry @Component struct CollapseDemo { build() { Column() { CustomCollapse({ title: '基础信息', defaultExpanded: true, // 默认展开 content: () => { // 自定义内容:表单示例 Column() { TextInput() .hint('请输入姓名') .padding(12) .border({ width: 1, color: '#eee' }) .borderRadius(4) .marginBottom(12); TextInput() .hint('请输入电话') .keyboardType(KeyboardType.PhoneNumber) .padding(12) .border({ width: 1, color: '#eee' }) .borderRadius(4); } } }); CustomCollapse({ title: '详情说明', content: () => { // 自定义内容:文本+图片 Column() { Text('这是折叠面板的详情内容,支持任意组件嵌套') .fontSize(14) .color('#666') .marginBottom(12); Image($r('app.media.ic_demo')) .width('100%') .aspectRatio(16/9) .objectFit(ImageFit.Cover) .borderRadius(4); } } }); } .padding(16) .backgroundColor('#fafafa'); } } Step 3:关键特性说明状态管理:通过@State维护内部展开状态,支持外部通过defaultExpanded初始化平滑动画:箭头旋转用transition,内容显隐用animateTo,统一 300ms 时长插槽设计:content参数接收函数,支持任意组件嵌套,灵活性拉满样式适配:使用flexGrow、aspectRatio等自适应属性,适配不同屏幕尺寸三、避坑指南(实战重点)1. 动画卡顿问题❌ 错误:直接用if-else控制内容显隐,无动画过渡✅ 正确:必须用animateTo包裹内容,且确保内容节点是连续的(避免 DOM 重建)2. 状态同步问题❌ 错误:外部修改状态时直接赋值isExpanded✅ 正确:如果需要外部控制,可将@State改为@Link,通过父组件传递状态3. 多端适配问题❌ 错误:固定width/height数值✅ 正确:使用vp单位(鸿蒙自适应单位),配合flex布局,避免硬编码4. 性能优化列表中使用时,给组件添加key属性:CustomCollapse(key: 'unique-key')避免内容中嵌套过重组件(如大图片),可懒加载:LazyForEach配合折叠面板四、进阶扩展方向支持手势滑动折叠:结合Gesture的PanGesture,实现上下滑动折叠添加折叠回调:增加onExpandChange参数,对外暴露展开 / 折叠状态变化自定义样式:支持传入titleColor、bgColor等参数,实现主题定制嵌套折叠:组件内部支持嵌套CustomCollapse,实现多级折叠面板 // 进阶:添加回调参数 interface CollapseProps { // ...其他参数 onExpandChange?: (expanded: boolean) => void; // 新增回调 } // 点击事件中触发回调 .onClick(() => { this.isExpanded = !this.isExpanded; this.props.onExpandChange?.(this.isExpanded); // 触发回调 });
-
import { router } from '@kit.ArkUI';import { call } from '@kit.TelephonyKit';import { util } from '@kit.ArkTS';import { City } from '../service/City';import { notificationManager } from '@kit.NotificationKit';interface JsonCity{ name:string}@Entry@Componentstruct Index { @State outCity:string = '隆回' @State goCity:string = '邵阳' @State date:string = '11月7日' @State isShow:boolean = false; @State isChange:boolean = false; @State isLoading:boolean = false; @State cityList:JsonCity[] =[] aboutToAppear(): void { this.sendNotice() this.getJsonCity() } async getJsonCity() { try { let rawFile = await getContext(this).resourceManager.getRawFileContent('city.json') let data = util.TextDecoder.create('utf-8').decodeWithStream(rawFile,{stream:false}) this.cityList = JSON.parse(data) }catch (e) { this.cityList = new City().getCityData() } } sendNotice() { notificationManager.requestEnableNotification() let res:notificationManager.NotificationRequest={ id:1, content:{ notificationContentType:notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal:{ title:'火车票', text:'欢迎回来' } }, badgeNumber:1 } notificationManager.publish(res) } build() { Stack(){ Column({space:10}){ Row(){ Text('火车票') .fontSize(18) .fontWeight('bold') .onClick(()=>{ router.push({ url:'pages/WebPage' }) }) Text('客服电话') .fontSize(18) .fontWeight('bold') .onClick(()=>{ call.makeCall('15842997527') }) } .width('100%') .backgroundColor('#fff') .padding(15) .justifyContent(FlexAlign.SpaceBetween) .borderRadius(10) Column({space:10}){ Row(){ Text(this.outCity) .fontSize(22) .fontWeight('bold') .onClick(()=>{ this.isShow = true; this.isChange = true; }) Image($r('app.media.startIcon')) .width(25) Text(this.goCity) .fontSize(22) .fontWeight('bold') .onClick(()=>{ this.isShow = true; this.isChange = false; }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) Text(this.date) .width('100%') .fontSize(16) .onClick(()=>{ DatePickerDialog.show({ selected:new Date(), onAccept:(v:DatePickerResult)=>{ this.date = `${Number(v.month) +1}月${v.day}日` } }) }) Button('查询车票') .width('100%') .onClick(()=>{ router.push({ url:'pages/InfoPage', params:{ outCity:this.outCity, goCity:this.goCity, date:this.date } }) }) } .width('100%') .padding(15) .backgroundColor('#fff') .borderRadius(10) } .width('100%') .backgroundColor('#45d7d7d7') .height('100%') .padding(10) .borderRadius(10) Column(){ Column(){ Row(){ Text(this.isChange ? "出发地" : '目的地') .fontSize(18) Image($r('app.media.startIcon')) .width(20) .onClick(()=>{ this.isShow = false }) } .width('100%') .height('10%') .justifyContent(FlexAlign.SpaceBetween) List(){ ForEach(this.cityList,(i:JsonCity)=>{ ListItem(){ Text(i.name) .width('100%') .padding(10) .textAlign(TextAlign.Center) .onClick(()=>{ if (this.isChange) { this.outCity = i.name }else { this.goCity = i.name } this.isShow = false }) } }) } .width('100%') .height('90%') } .width('80%') .height('60%') .backgroundColor('#fff') .borderRadius(10) .margin({top:'40%'}) .padding(10) } .width('100%') .backgroundColor('#45d7d7d7') .height('100%') .visibility(this.isShow ? Visibility.Visible : Visibility.Hidden) } .height('100%') .width('100%') }}import { router } from '@kit.ArkUI'import { NetWorkService, SeatInfo, TrainInfo } from '../service/NetWorkService'@Entry@Componentstruct InfoPage { @State outCity:string = '隆回' @State goCity:string = '邵阳' @State date:string = '11月7日' @State TrainInfo:TrainInfo[] = [] @State TrainSeats:SeatInfo[] = [] @State netWork:NetWorkService = new NetWorkService() onPageShow(): void { this.getPageInfo() this.sendInfo() } async sendInfo() { try { await this.netWork.getAuthToken() let res = await this.netWork.sendInfo(this.outCity,this.goCity,this.date); this.TrainInfo = res.trains }catch (e){ } } getPageInfo() { let data = router.getParams() as Record<string,string>; this.outCity = data?.['outCity'] this.goCity = data?.['goCity'] this.date = data?.['date'] } build() { Column({space:10}){ Row(){ Text(this.outCity + "->" + this.goCity) .fontSize(18) .fontWeight('bold') Text(this.date) .fontSize(18) .fontWeight('bold') } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .padding(10) .backgroundColor('#fff') .borderRadius(10) Column({space:10}){ List(){ ForEach(this.TrainInfo,(i:TrainInfo)=>{ ListItem(){ Column(){ Row(){ Text(i.train_number) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) Row(){ Text(i.departure_time) .fontSize(16) .fontWeight('bold') Column(){ Text("历时") .fontSize(16) Text(i.duration + '分钟') .fontSize(14) } Text(i.arrival_time) .fontSize(16) .fontWeight('bold') } .width('100%') .justifyContent(FlexAlign.SpaceBetween) Row(){ ForEach(i.seats,(j:SeatInfo)=>{ Row(){ Text(j.type + ':') .fontSize(12) Text(j.price + '元 ') .fontSize(12) Text(j.available ? '有座' : '无座') .fontSize(12) } }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) } .backgroundColor('#5ed7d7d7') .width('100%') .padding(10) .borderRadius(10) .margin({bottom:10}) } }) } .width('100%') .height('100%') } .width('100%') .layoutWeight(1) .backgroundColor('#fff') .borderRadius(10) .padding(10) } .padding(10) .height('100%') .width('100%') .backgroundColor('#45d7d7d7') }}import { http } from "@kit.NetworkKit";export interface TrainResponse{ trains:TrainInfo[]}export interface TrainInfo{ train_number:string, type:string, departure_time:string, arrival_time:string, duration:string, seats:SeatInfo[],}export interface SeatInfo{ type:string, price:string, available:string}export class NetWorkService{ private token:string = ''; private httpClient = http.createHttp(); async getAuthToken(){ let req = { "auth": { "identity": { "methods": [ "password" ], "password": { "user": { "domain": { "name": "hid_bcwyicen9qp1qc8" }, "name": "ch1", "password": "ch123456" } } }, "scope": { "domain": { "id": "7cd1794261ec444aace6ca7042addf04", "name": "hid_bcwyicen9qp1qc8" }, "project": { "id": "6a4e7e844b8941ba952cbf9805a97469", "name": "cn-north-4" } } } } let res = await this.httpClient.request( 'https://iam.cn-north-4.myhuaweicloud.com/v3/auth/tokens', { method:http.RequestMethod.POST, header:{ 'Content-Type':'application/json' }, extraData:req } ) if(res.responseCode === 201){ let headers = res.header as Record<string,string> this.token = headers?.['x-subject-token'] return true }else { return false } } async sendInfo(outCity:string,goCity:string,date:string):Promise<TrainResponse>{ let req = { inputs: { query: `帮我查询${date}从${outCity}站到${goCity}站的火车高铁票信息,要求有时间车次票价,几等座剩余情况.将请求到的数据严格json文件格式返回,不要包括任何其他文本` } } let res = await this.httpClient.request( "https://123.249.99.67/v1/6a4e7e844b8941ba952cbf9805a97469/agents/2304f807-97e6-4367-9ae3-077db9ff67cf/conversations/ec476ece-a9cd-4d37-b79d-2ce577760089?version=1762233862585", { method:http.RequestMethod.POST, header:{ 'Content-Type':'application/json', 'X-Auth-Token':this.token }, extraData:req } ) if (res.responseCode === 200) { let lines = (res.result as string).split('\n'); for (let line of lines){ if (line.trim().startsWith('data:')) { let str = line.substring(5).trim(); let data = JSON.parse(str) if (data.event === 'summary_response') { let JsonMatch = (data.content).match(/```json\n([\s\S]*?)\n```/) if (JsonMatch && JsonMatch[1]){ let content = JsonMatch[1].trim() if (content) { let jsonData = JSON.parse(content) console.log("解析数据",JSON.stringify(jsonData)) return jsonData } } } } } } }}"requestPermissions": [ {"name": "ohos.permission.INTERNET"}, {"name": "ohos.permission.NOTIFICATION_CONTROLLER"}],
-
还没报名吗?点击下方链接或者扫描图片二维码快来报名吧!报名链接:cid:link_0第1步:输入182;第2步:确认赛项名称;第3步:进入报名;第4步:提交盖章版报名表
-
一、问题说明在原生Slider组件基础上,丰富滑动组件功能,如在滑动组件上打若干点,通过自定义滑动组件实现功能,实现思路:Stack布局包含两个子组件(Slide组件和实现打点功能的组件),结果出现两个端点的打点始终不能与Slider的端点重合。二、原因分析1.先通过ArkUI观察各组件视图大小信息slider组件视图信息:根据视图信息发现,slider组件是填充父布局的,宽度100%自定义打点视图组件视图信息:打点视图组件填充父布局,宽度100%,但是却展示了两个样式,继续排查代码2.排查代码间距相关设置代码内没有设置内外间距的属性3.查看官方文档总结:当Slider水平方向显示时,存在左右间距导致Slider可视部分不能填充父布局三、解决思路1.根据Slider的style属性,动态计算Slider水平方向显示时的两侧间距2.根据上述1计算的间距,动态调整自定义打点组件的两侧间距四、解决方法1.计算Slider水平方向显示时的两侧间距/** * 获取Slider组件两侧间距 * @param sliderStyle * @returns */private getPaddingSpace(sliderStyle: SliderStyle): number { if (SliderStyle.OutSet == sliderStyle) { //The slider is on the slide rail. return 9 } else if (SliderStyle.InSet == sliderStyle) { //The slider is in the slide rail. return 6 } return 0}2.自定义打点组件设置内间距Row() { //自定义打点组件实现细节}.alignItems(VerticalAlign.Center).hitTestBehavior(this.hitTest).width('100%').padding({ left: this.paddingSpace, right: this.paddingSpace })3.实现效果如图五、方案成果总结 当我们的问题分析逻辑上出现矛盾时或者出现疑问时,可首先快速查阅文档,减少时间的消耗,或者到官方文档行业常见问题页面进行查找、方案筛选。 理解原生设计Slider基础上,在对原生Slider上自定义组件或者对组件功能进行扩展时,根据两侧存在的间距、style属性动态调整宽度,完成功能的拓展。
-
在鸿蒙应用持续迭代的过程中,业务需求变化常常伴随着数据库表结构的变更(如增加新字段、修改字段类型等)。如果处理不当,特别是在跨版本升级时,极易引发数据丢失、应用崩溃等严重生产问题。本文将深入分析这一问题,并提供一套完整的解决方案。问题说明问题场景:某鸿蒙应用在发布V2.0版本时,为了新增一个功能,需要在原有的用户信息表 user 中增加一个 nickname(昵称)字段。开发者在本地测试后正常发布。生产问题:部分已安装V1.0版本的老用户升级到V2.0后,启动应用时出现闪退。更严重的是,成功启动应用后,部分用户发现原有的账户数据(如收藏列表、浏览记录)全部丢失,变成了一个新用户的状态。原因分析该问题的根源在于数据库版本升级策略的缺失或错误实现。默认行为误区:鸿蒙的 RDB 商店在打开数据库时,如果指定的版本号 (version) 比当前数据库的版本号高,会自动触发 upgrade 操作。然而,如果没有为upgrade显式地编写迁移逻辑,数据库框架会默认采用“删除旧表,创建新表”的策略。这就是导致用户数据被清空的直接原因。错误的升级操作:开发者可能在 onUpgrade 回调中只写了创建新表的SQL(CREATE TABLE IF NOT EXISTS …),而没有处理旧数据的迁移。例如:onUpgrade: (db, oldVersion, newVersion) => {// 只有创建新表的语句,没有数据迁移逻辑db.executeSql(“CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY, name TEXT, nickname TEXT)”);// 此时,老表已经被丢弃,其中的数据全部丢失!}版本管理混乱:应用有多个历史版本,每个版本可能都有不同的数据库结构。升级路径可能不是从 v1 直接到 v2,也可能是从 v1 到 v3,或者从 v2 回退到 v1。升级脚本如果没有考虑所有可能的升级路径,就会出错。解决思路解决此问题的核心思路是:在数据库结构变更时,必须编写安全的数据迁移脚本,将旧数据库中的数据无损地迁移到新结构的数据库中。预案与回滚:立即下线有问题的V2.0版本,推送一个修复前的V1.1版本(版本号高于V1.0但低于V2.0),优先阻止问题影响更多用户。如果用户数据在客户端有加密或备份,尝试引导用户进行数据恢复。彻底修复方案:规范版本管理:为数据库定义严格的版本号,每次表结构变更,版本号必须+1。实现增量升级:在 onUpgrade 方法中,根据 oldVersion 和 newVersion 的差异,编写一步一步的升级脚本,确保从任何一个历史版本升级到最新版本都能正确执行。使用ALTER TABLE语句:优先使用 ALTER TABLE … ADD COLUMN 等标准SQL语句来新增字段,避免重建表。复杂变更分步处理:对于无法用 ALTER 语句完成的变更(如修改字段类型、删除字段),需要创建临时表,将旧表数据导入临时表,删除旧表,再创建新表,最后将数据从临时表导回新表。解决方案以下是在鸿蒙应用中正确实现数据库升级的代码示例。定义数据库版本常量export const DB_CONFIG = {NAME: ‘myApp.db’,VERSION_1: 1, // 初始版本VERSION_2: 2, // 新增nickname字段CURRENT_VERSION: 2};2. 正确的onUpgrade实现import { relationalStore } from ‘@ohos.data.relationalStore’;import { DB_CONFIG } from ‘./DatabaseUtils’;const STORE_CONFIG = {name: DB_CONFIG.NAME,securityLevel: relationalStore.SecurityLevel.S1};// 获取RDB连接relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, db) => {if (err) {console.error(Failed to get RdbStore. Code:${err.code}, message:${err.message});return;}console.info(‘Succeeded in getting RdbStore.’);// 版本变化时,会触发Upgrade db.version = DB_CONFIG.CURRENT_VERSION; db.on('versionChange', (event) => { // 版本变更事件,通常在这里不处理具体逻辑,但可以监听 console.info(`Version changed from ${event.oldVersion} to ${event.newVersion}`); }); // 核心:数据库升级处理 db.upgrade((db, oldVersion, newVersion, callback) => { console.info(`Starting database upgrade from v${oldVersion} to v${newVersion}`); /* 使用事务保证升级操作的原子性 */ db.beginTransaction() .then(() => { // 根据旧版本号,逐步升级 if (oldVersion < 2) { // 从v1升级到v2:增加nickname字段 console.info('Upgrading database from v1 to v2: ADD COLUMN nickname'); return db.executeSql("ALTER TABLE user ADD COLUMN nickname TEXT"); } // 未来如果升级到v3,可以在这里添加 else if (oldVersion < 3) {...} }) .then(() => { db.commit(); // 提交事务 callback(); // 升级成功,调用callback console.info('Database upgrade completed successfully.'); }) .catch((err) => { db.rollback(); // 升级失败,回滚事务 console.error(`Database upgrade failed. Code:${err.code}, message:${err.message}`); // 回调失败,数据库打开会失败,避免用户使用一个有问题的数据库 throw err; }); });});3. 复杂变更的示例(需要重建表)假设V3版本需要将表 user 中的 name 字段拆分为 first_name 和 last_name,无法用 ALTER 完成。
-
一、问题说明点播页面暂停出现推荐广告,点击推荐广告跳转直播页面,出现两个播放声音,即退出直播页面后节目仍然在播放,系统软件版本:5.1.0.150二、原因分析1.检查页面不可见时,有无其他逻辑或者问题导致播放器未释放播放资源;2.检查路由跳转,是否跳转了多次;3.检查画中画,是否画中画持有了页面4.检查路由相关可能导致问题逻辑三、解决思路1.在播放器播放、释放处增加断点,当直播页面返回时,发现断点到了释放处,没有再拉起播放,排除此种情况;2.路由处增加断点,当点击跳转时路由也只走了一次,但是在直播页面的aboutToAppear生命周期中断点发现,生命周期却执行了两次,总结:点击了一次,路由栈push一次,目标页面生命周期却执行了两次;3.注释画中画所有功能方法实现后仍发现此问题存在,排除画中画问题;4.基于上述2的问题分析,我们继续沿着路由栈相关线索继续定位,在目标页面的aboutToAppear生命周期中的首行发现有移除上个页面的逻辑,把此处代码注释,重新编译、运行程序后按照步骤不再存在音频泄漏问题总结:基于2、4的分析线索,我们按照代码逻辑、单独新建工程,按照场景进行确定、验证问题。四、解决方法1.Demo梳理新建工程,在主工程主页面放置一个按钮(主页面我们简称Main界面);点击此按钮进行页面跳转(跳转至目标页面我们简称PageOne页面),在PageOne页面aboutToAppear生命周期中首行增加日志表示已执行;在PageOne页面再次进行路由跳转(跳转至目标页面我们简称PageTwo页面),在PageTwo页面aboutToAppear生命周期中首行增加日志表示已执行,日志后面增加PageOne页面的移除操作。一共三个页面,分别为Main页面、PageOne页面、PageTwo页面。2.代码示例1.主页面跳转至PageOne页面Main页面:build() { Navigation(MGRouterManager.pathStack) { Text('点击跳转至PageOneNav') .translate({ x: this.translateX }) .fontColor(Color.Black) .fontSize(25) .margin({ top: 60 }) .backgroundColor(Color.Orange) .onClick(() => { MGRouterManager.pathStack.pushPathByName('PageOneNav', false) }) }}PageOne页面:import { MGRouterManager } from './MGRouterRule'@Componentexport struct PageOneNav { @State prams: string = 'PageOneNav' aboutToAppear(): void { console.log('PageOneNav', ` PageOneNav已执行 `) } build() { NavDestination() { Text(this.prams).fontColor(Color.Black).fontSize(25).margin({ top: 200 }).onClick(() => { MGRouterManager.pathStack.pushPathByName('PageTwoNav', false) }) }.onShown(() => { MGRouterManager.pathStack.getAllPathName().forEach((item) => { console.log(' FirstPageNav--getAllPathName ', item) }) }).onBackPressed(() => { return false }) }}@Builderexport function PageOneNavBuilder() { PageOneNav()}主页面跳转至PageOne页面页面后,aboutToAppear生命周期中日志打印一次:2.PageOne页面跳转至PageTwo页面@Componentexport struct PageTwoNav { aboutToAppear(): void { Test.instance.num++ console.log('PageTwoNav', ` PageTwoNav执行次数 ${Test.instance.num++}`) MGRouterManager.pathStack.removeByName('PageOneNav') //移除栈 } build() { NavDestination() { Text('SecondPageNav').fontColor(Color.Black).fontSize(25) }.onShown(() => { }).onBackPressed(() => { return false }) }}@Builderexport function PageTwoNavBuilder() { PageTwoNav()}PageOne页面跳转至PageTwo页面,PageTwo页面aboutToAppear生命周期中日志执行了两次:3.我们把移除栈的代码注释,发现PageOne页面跳转至PageTwo页面,PageTwo页面aboutToAppear生命周期中日志只执行了一次:4.总结:PageOne页面跳转至PageTwo页面,在PageTwo页面aboutToAppear生命周期中首行执行移除上个页面栈的操作会导致PageTwo页面生命周期执行两次。5.‘目前’解决方案:可以在路由跳转前进行移除操作,或者跳转至目标页面后,在能够表示延迟的逻辑里执行移除操作,不是写setTimeout延迟处理。
-
HarmonyOS WebView 调试指南:使用Chrome DevToolsWeb组件支持使用DevTools工具调试前端页面。DevTools是Web前端开发调试工具,支持在电脑上调试移动设备前端页面。开发者通过setWebDebuggingAccess()接口开启Web组件前端页面调试能力,使用DevTools在电脑上调试移动前端网页,设备需为4.1.0及以上版本。前置准备在开始调试前,请确保完成以下配置:安装HDC工具:[Mac用户配置指南][Windows用户配置指南]验证安装:终端执行 hdc -v 输出版本 Ver: 3.1.0e 表示已安装 输出提示 command not found 检查是否安装或是否配置环境变量 四步调试流程第一步:获取应用进程IDhdc shell ps -ef | grep "您的应用包名" 示例输出解析:u0_a200 12345 678 0 10:00:00 ? 00:00:00 com.example.app12345 就是需要的进程ID🚨 重要提示:执行此命令时请确保:只连接一台HarmonyOS设备关闭所有模拟器第二步:建立调试隧道hdc fport tcp:9222 localabstract:webview_devtools_remote_进程ID成功执行后终端显示 Forwardport result:OK🚨 这个经常因锁屏等断联秩序执行上面命令第三步:Chrome调试配置打开Chrome访问:chrome://inspect点击"Configure" → 添加 localhost:9222保持终端命令运行,刷新页面在"Remote Target"区域找到您的应用 → 点击"inspect"第四步:结束调试hdc fport kill tcp # 关闭所有端口转发 # 或 hdc fport list # 查看当前转发情况 常见问题解答❓ 为什么看不到设备?检查HDC是否识别设备:hdc list targets确保没有多设备连接确认端口转发命令正在运行❓ 调试界面空白?尝试关闭Chrome所有实例重新打开检查HarmonyOS应用是否在前台运行❓ 多设备错误提示?[Fail]ExecuteCommand need connect-key? please confirm a device by help info解决方案:断开其他设备,确保只连接一台调试设备调试技巧元素检查:实时修改DOM和CSS控制台调试:直接执行JavaScript代码网络分析:监控所有网络请求性能分析:检测页面渲染性能通过以上步骤,您可以轻松实现HarmonyOS WebView页面的实时调试,显著提升开发效率!
-
通过分析提供的Swiper组件代码,结合HarmonyOS开发经验,针对该实现方案进行如下技术解析与优化建议:import { INDEX_DATA } from '../../mock/ProductsData'; import { SwiperModel } from '../../model/GoodsModel'; const isNewSlide: boolean = true; @Component export struct SwiperComponent { @State breakPoints: string | undefined = AppStorage.get('breakPoint'); @State currentIndex: number = 2; private uiContext: UIContext | undefined = undefined; /** * Get the image offset coefficients. * * @param index * @returns offset coefficients */ getImgCoefficients(index: number): number { let coefficient: number = this.currentIndex - index; let tempCoefficient: number = Math.abs(coefficient); if (tempCoefficient <= 2) { return coefficient; } let dataLength: number = INDEX_DATA.length; let tempOffset: number = dataLength - tempCoefficient; if (tempOffset <= 2) { if (coefficient > 0) { return -tempOffset; } return tempOffset; } return 0; } /** * Get the image offset. * * @param index * @returns offset */ getOffSetX(index: number): number { let offsetIndex: number = this.getImgCoefficients(index); let tempOffset: number = Math.abs(offsetIndex); let offsetX: number = 0; if (tempOffset === 1) { offsetX = -40 * offsetIndex; } return offsetX; } startAnimation(isLeft: boolean): void { this.uiContext?.animateTo({ duration: 300, }, () => { let dataLength: number = INDEX_DATA.length; let tempIndex: number = isLeft ? this.currentIndex + 1 : this.currentIndex - 1 + dataLength; this.currentIndex = tempIndex % dataLength; }) } build() { if (!isNewSlide) { } else { Column() { Stack() { ForEach(INDEX_DATA, (item: SwiperModel, index: number) => { Row() { Image(item.img) .objectFit(ImageFit.Fill) .borderRadius(8) .width(this.breakPoints === 'sm' ? 340 : 260) .height(index !== this.currentIndex ? "65%" : `${95 - 13 * Math.abs(this.getImgCoefficients(index))}%`) .opacity(1 - 0.2 * Math.min(2, Math.abs(this.getImgCoefficients(index)))) } .borderRadius(8) .offset({ x: this.getOffSetX(index), y: 0 }) .blur(10 * Math.abs(this.getImgCoefficients(index))) .zIndex(index !== this.currentIndex && this.getImgCoefficients(index) === 0 ? 0 : 2 - Math.abs(this.getImgCoefficients(index))) }, (item: SwiperModel) => JSON.stringify(item)) } .alignContent(Alignment.Center) .gesture( PanGesture({ direction: PanDirection.Horizontal }) .onActionStart((event: GestureEvent) => { this.startAnimation(event.offsetX < 0); }) ) } .width('100%') .height('25%') } } } 一、问题说明循环逻辑缺陷代码通过取模运算tempIndex % dataLength实现循环切换,但未开启Swiper组件的loop属性,导致:边缘位置切换时缺少平滑过渡动画手势操作与组件内置循环机制冲突无法通过SwiperController实现程序化控制动画性能问题自定义的PanGesture手势与animateTo实现存在:手势响应与动画帧率不同步缺少触摸速率检测导致滑动惯性缺失多层级嵌套动画可能引发渲染管线阻塞布局计算冗余getImgCoefficients/getOffSetX方法中:重复计算数据长度dataLength未考虑设备像素密度差异硬编码的40px偏移量导致多分辨率适配问题二、原因分析混合实现方案冲突同时使用原生Swiper组件特性与自定义手势控制,导致:内置布局机制与手动offset计算产生叠加效应zIndex层级管理未考虑组件渲染顺序breakPoints响应式逻辑未与Swiper属性联动状态管理不足@State修饰的currentIndex缺乏:与Swiper组件index属性的双向绑定防抖机制导致快速滑动时状态不同步未持久化存储当前浏览位置三、解决思路3.1 循环逻辑重构原生能力整合启用Swiper组件的loop属性替代手动计算循环逻辑,利用其内置的虚拟化渲染机制,避免边缘切换时的跳跃问题。结合displayCount属性控制预加载数量,平衡内存占用与流畅性(如设置displayCount: 3)。双向状态同步使用SwiperController与@Link装饰器实现currentIndex与Swiper组件索引的双向绑定,确保手势操作与程序化控制的一致性。3.2 动画与手势优化性能敏感型动画将animateTo替换为swipeTo方法,直接触发Swiper内置动画引擎,避免嵌套动画导致的帧率波动。引入Curve.EaseOut缓动曲线模拟自然滑动惯性,提升交互真实感。多点触控兼容添加PanGesture的priority参数配置,避免与页面滚动或其他手势冲突。通过GestureGroup实现多手势协同,支持滑动过程中轻触暂停等复合操作。3.3 工程化增强响应式设计规范抽取breakPoints相关参数至Resource资源文件,通过@ohos.mediaquery监听屏幕变化实现动态布局。使用GridContainer替代硬编码宽高值,适配折叠屏、平板等多形态设备。内存泄漏防护在aboutToDisappear生命周期中注销MediaDataHandler监听,并通过@Track装饰器标记关键对象引用链。采用LazyForEach替代ForEach实现图片懒加载,结合RecycleView复用机制降低内存峰值。3.4 视觉一致性提升动态模糊策略根据zIndex层级动态调整blur值,避免固定模糊系数导致的视觉割裂。引入LinearGradient实现边缘渐变遮罩,增强轮播内容的沉浸感。无障碍适配为Swiper子项添加accessibilityLabel描述,支持屏幕朗读工具识别轮播内容。四、解决方案启用原生循环特性调整组件声明:Swiper() { ForEach(...) } .loop(true) .autoPlay(false) .displayCount(3) // 根据搜索结果5优化显示数量 优化动画实现private swiperController: SwiperController = new SwiperController(); startAnimation(isLeft: boolean) { const targetIndex = this.calculateTargetIndex(isLeft); this.swiperController.showNext({ speed: 300, // 与animateTo duration保持一致 curve: Curve.Ease }); this.currentIndex = targetIndex; } 响应式布局改进@Builder swiperItemBuilder(item: SwiperModel) { Image(item.img) .aspectRatio(1.78) // 保持16:9比例 .width(this.breakPoints === 'sm' ? '80%' : '60%') .height(this.breakPoints === 'sm' ? '70%' : '90%') .margin({ left: $r('app.float.swiper_margin'), right: $r('app.float.swiper_margin') }) }
-
一、 问题说明:应用通知的困境作为一名HarmonyOS开发者,你是否曾遇到过以下痛点?重要通知被淹没:应用内生成了一张即将过期的优惠券或一个待支付的账单,你希望用户能及时看到,但传统的推送通知极易被其他信息淹没,导致用户错过。被动等待用户:传统的交互模式是“人找服务”,应用只能被动等待用户打开,无法主动在恰当时机为用户提供恰到好处的服务。体验割裂:即使用户点击通知,跳转回应用的体验也可能不够流畅,无法直接定位到相关页面。这些问题背后的核心是:应用缺乏一种与系统深度集成、由系统智慧决策并主动推荐服务的能力。二、 原因分析与解决思路原因分析:上述问题的根源在于传统通知机制是“单向广播”和“平等竞争”的。所有应用的通知都涌入同一个通知中心,缺乏上下文感知和智能调度,无法在最佳时机触达用户。解决思路:HarmonyOS的意图框架(Intents Kit) 提供了完美的解决方案。它的核心思想是:应用共享意图:应用将重要的业务事件(我们称之为“意图”,如“查看还款”、“使用优惠券”)结构化地共享给系统智慧分发平台。系统智能决策:系统平台会综合上下文(如时间、地点、用户习惯)进行智能分析,在最合适的时机(如还款日当天早上、到达商场附近时)主动向用户推荐一张可交互的提醒卡片。用户一键直达:用户点击卡片后,即可无缝跳转回应用内的对应详情页,实现“服务找人”的沉浸式体验。这不仅能极大提升关键信息的触达率,更能打造一种“润物细无声”的智能化用户体验。三、 解决方案:四步开发集成意图框架下面我们一步步来看如何为你的HarmonyOS应用集成这一能力。第一步:端侧注册意图首先,你需要在应用中声明你希望处理哪些“意图”。创建配置文件:在项目的 entry/src/main/resources/base/profile/ 目录下创建 insight_intent.json 文件。配置多个意图:在此文件中,以数组形式声明所有需要支持的意图。请注意,意图名称必须是系统预置的,不能自定义。// insight_intent.json{“insightIntents”: [{“intentName”: “ViewRepayment”, // 预置意图:查看还款“domain”: “BankingDomain”, // 所属业务垂域“intentVersion”: “1.0.1”, // 意图版本号“srcEntry”: “./ets/entryability/InsightIntentExecutorImpl.ets”, // 执行器入口“uiAbility”: {“ability”: “EntryAbility”,“executeMode”: [“background”, “foreground”] // 执行模式}},{“intentName”: “CheckCoupon”, // 预置意图:查看优惠券“domain”: “ShoppingDomain”,“intentVersion”: “1.0.0”,“srcEntry”: “./ets/entryability/InsightIntentExecutorImpl.ets”,“uiAbility”: {“ability”: “EntryAbility”,“executeMode”: [“foreground”]}}]}第二步:实现意图执行器系统在用户点击卡片后,会调用你注册的执行器。你需要在此处理跳转逻辑。// InsightIntentExecutorImpl.etsimport { insightIntent, InsightIntentExecutor } from ‘@kit.AbilityKit’;import { window } from ‘@kit.ArkUI’;import { BusinessError } from ‘@kit.BasicServicesKit’;export default class InsightIntentExecutorImpl extends InsightIntentExecutor {private static readonly VIEW_REPAYMENT = ‘ViewRepayment’;private static readonly CHECK_COUPON = ‘CheckCoupon’;// 处理卡片点击事件(前台模式)onExecuteInUIAbilityForegroundMode(intentName: string, param: Record<string, Object>, pageLoader: window.WindowStage): Promise<insightIntent.ExecuteResult> {switch (intentName) {case InsightIntentExecutorImpl.VIEW_REPAYMENT:return this.handleViewRepayment(param, pageLoader); // 跳转至还款页case InsightIntentExecutorImpl.CHECK_COUPON:return this.handleCheckCoupon(param, pageLoader); // 跳转至优惠券页default:return Promise.resolve({ code: -1, result: { message: ‘未知意图’ } });}}private handleViewRepayment(param: Record<string, Object>, pageLoader: window.WindowStage): Promise<insightIntent.ExecuteResult> {// 1. 解析param中的业务参数(如订单号)// 2. 携带参数,跳转到应用内的还款详情页let pageParams: Record<string, string> = { ‘repaymentInfo’: JSON.stringify(param) };let localStorage: LocalStorage = new LocalStorage(pageParams);return pageLoader.loadContent(‘pages/RepaymentDetailPage’, localStorage).then(() => ({ code: 0 })).catch((error: BusinessError) => ({ code: -1, result: { message: error.message } }));}private handleCheckCoupon(param: Record<string, Object>, pageLoader: window.WindowStage): Promise<insightIntent.ExecuteResult> {// … 类似逻辑,跳转到优惠券详情页return Promise.resolve({ code: 0 });}}第三步:云侧配置与交互意图框架需要云侧协同工作。平台配置:在AppGallery Connect上架你的应用,并在小艺开放平台为你的每个意图提交配置申请,等待审核通过。获取凭证(SID):在端侧调用API获取Service OpenID(SID),这是云侧操作的凭证。// 在应用合适的位置调用insightIntent.getSid(context, false).then((sid: string) => {console.info(‘获取到的SID:’, sid);// 将SID发送给你的服务器});捐赠事件:当业务事件发生时(如用户生成订单),你的应用服务器需要调用华为的接口,将事件数据(遵循预置意图的Schema格式)和SID一起发送给智慧分发平台。事件撤销:如果事件过期,你的服务器应及时通知平台撤销推荐。第四步:处理意图调用这一步已在第二步的意图执行器中自动完成。你无需额外编码,系统会在用户点击卡片后自动调用你已实现的 onExecuteInUIAbilityForegroundMode 方法,并执行你写好的跳转逻辑。
-
一、问题说明**文字位置偏移问题:**绘制文字时出现位置偏移,特别是在设置旋转角度后,文字坐标未正确对齐**复合样式支持缺失:**当前只能处理单一字体样式(如单独斜体或加粗),无法支持组合样式**文字测量不准确:**markHeight始终为0,无法正确获取文字高度**透明度叠加异常:**全局透明度设置会影响后续绘制操作**性能问题:**离屏画布尺寸固定导致大图水印质量下降二、原因分析rotate()方法未配合translate()调整坐标系原点fillText方法的y坐标基准线未统一设置多次绘制未重置上下文状态switch-case结构仅处理单一枚举值字体字符串拼接未采用标准格式measureText()返回的TextMetrics对象未正确解析未考虑字体metrics实际渲染特性globalAlpha属性未在每次绘制前重置透明度计算未考虑颜色本身的alpha通道三、解决思路使用save()/restore()隔离每次绘制状态设置统一的textBaseline基准线采用坐标系平移配合旋转改用位运算判断样式组合遵循font属性格式规范:[style] [weight] size family四、解决方案文字位置偏移问题优化:offScreenContext.save(); offScreenContext.translate(startX, startY); // 设置绘制起点 offScreenContext.rotate(-config.dRotationAngle * Math.PI / 180); offScreenContext.textBaseline = 'alphabetic'; // 统一基准线 // ...绘制操作 offScreenContext.restore(); 复合样式支持缺失优化:let fontStyle = ''; if (config.iWordFontStyle & FontStyleFlag.ITALIC) fontStyle += 'italic '; if (config.iWordFontStyle & FontStyleFlag.BOLD) fontStyle += 'bold '; offScreenContext.font = `${fontStyle}${config.iWordFontSize}px ${config.strWordFontName}`; 文字测量优化:const metrics = offScreenContext.measureText(text); const actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; const lineYPos = metrics.fontBoundingBoxAscent; // 计算基准线位置 透明度叠加异常优化:// 改用RGBA颜色设置替代全局透明度 const alpha = config.lvpWmWords.iTransparent / 100; offScreenContext.fillStyle = `rgba(${r},${g},${b},${alpha})`; // 镂空效果采用复合路径 offScreenContext.save(); offScreenContext.clip(textPath); // 创建文字路径 offScreenContext.clearRect(0, 0, canvasW, canvasH); // 镂空处理 offScreenContext.restore(); 性能优化:// 动态计算画布尺寸 const canvasW = markWidth * 1.2; const canvasH = actualHeight * 1.5; const offScreenCanvas = new OffscreenCanvas(canvasW, canvasH); // 添加设备像素比适配 const dpr = window.devicePixelRatio; offScreenCanvas.width *= dpr; offScreenCanvas.height *= dpr; offScreenContext.scale(dpr, dpr); 异常处理增强:try { await pixelMap2Buffer(bmp, localId); } catch (err) { console.error(`Watermark generation failed: ${err.code} ${err.message}`); throw new Error('WATERMARK_RENDER_ERROR'); }
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签