-
1.1 问题说明在求职招聘、社交交友、内容推荐等应用场景中,用户需要快速浏览并筛选大量信息(如推荐岗位、用户资料)。传统的列表式(List)浏览虽然信息密度高,但在“快速决策”和“趣味性”上略显不足。用户期望一种类似“探探”或“BOSS直聘”的交互方式:通过左右滑动卡片来表达“喜欢/投递”或“无感/跳过”,同时获得流畅的视觉反馈。本案例基于 HarmonyOS 的原生组件与动画能力,实现高性能的层叠卡片滑动切换效果。1.2 原因分析交互直觉性滑动(Swipe)是移动端最直观的手势,能有效降低用户的决策成本。视觉层级构建需要通过层叠关系(Stack)和透视效果(Scale/Opacity)来突出当前内容,弱化背景内容,引导用户聚焦。性能挑战连续的滑动与卡片复用、即时的布局刷新对渲染性能有要求。若使用非原生动画或频繁触发布局重算,易导致卡顿。1.3 解决思路· 布局策略 :采用 Stack 容器进行层叠布局。通过数据驱动,动态计算每个卡片的 zIndex 、 scale (缩放)、 opacity (透明度)和 margin/offset (偏移),形成“前大后小、前实后虚”的视觉纵深。· 手势驱动 :使用 PanGesture 绑定顶层卡片,实时追踪手指移动距离,更新卡片的 X/Y 轴偏移量和旋转角度。· 动画衔接 :利用 animateTo 显式动画接口。当手指松开时,判断滑动距离是否超过阈值:· 超过阈值 :触发“飞出”动画,将卡片移出屏幕,随后更新数据源(移除该项),底层卡片通过动画自动补位。· 未超阈值 :触发“回弹”动画,卡片复位。1.4 解决方案import { curves } from '@kit.ArkUI'; // 模拟卡片数据接口interface CardItem { id: number; text: string; color: Color;} @Entry@Componentstruct SwipeCardPage { // 卡片数据源 @State cardList: CardItem[] = [ { id: 1, text: '高级前端开发工程师', color: Color.Blue }, { id: 2, text: 'HarmonyOS 架构师', color: Color.Red }, { id: 3, text: 'UI/UX 设计师', color: Color.Orange }, { id: 4, text: '产品经理', color: Color.Green }, { id: 5, text: '测试开发专家', color: Color.Pink }, ]; // 顶层卡片偏移量 @State offsetX: number = 0; @State offsetY: number = 0; // 顶层卡片旋转角度 @State rotateAngle: number = 0; // 移除卡片的阈值 private readonly SWIPE_THRESHOLD: number = 120; build() { Column() { // 层叠布局容器 Stack() { // 遍历渲染卡片(注意:Stack中后定义的在上面,通常需要反向或控制zIndex,这里简化为只渲染前3个) ForEach(this.cardList.slice(0, 3).reverse(), (item: CardItem, index: number) => { this.CardView(item, this.cardList.indexOf(item)) }, (item: CardItem) => item.id.toString()) } .width('100%') .height('60%') .alignContent(Alignment.Center) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .backgroundColor('#F1F3F5') } @Builder CardView(item: CardItem, index: number) { // 动态计算样式 // index=0 是当前显示的最顶层卡片 Row() { Text(item.text) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(Color.White) } .width('80%') .height('60%') .backgroundColor(item.color) .borderRadius(20) .shadow({ radius: 20, color: 'rgba(0,0,0,0.2)', offsetY: 10 }) .justifyContent(FlexAlign.Center) // 核心样式逻辑 .scale(this.getScale(index)) .opacity(this.getOpacity(index)) .translate(this.getTranslate(index)) .zIndex(this.cardList.length - index) // 保证index 0 在最上层 .rotate({ angle: index === 0 ? this.rotateAngle : 0, z: 1 }) // 仅顶层卡片旋转 // 绑定手势(仅给顶层卡片绑定) .gesture( index === 0 ? PanGesture() .onActionUpdate((event: GestureEvent) => { // 1. 跟随手指移动 this.offsetX = event.offsetX; this.offsetY = event.offsetY; // 2. 根据移动距离计算旋转角度,增加动感 this.rotateAngle = this.offsetX * 0.1; }) .onActionEnd(() => { // 3. 手势结束,判断是否移除 if (Math.abs(this.offsetX) > this.SWIPE_THRESHOLD) { // 移出屏幕动画 animateTo({ duration: 300, curve: Curve.FastOutLinearIn, onFinish: () => { // 动画结束后移除数据,重置状态 this.cardList.shift(); this.offsetX = 0; this.offsetY = 0; this.rotateAngle = 0; }}, () => { // 这里的偏移量要足够大,滑出屏幕 this.offsetX = this.offsetX > 0 ? 500 : -500; this.rotateAngle = this.offsetX > 0 ? 45 : -45; this.offsetY = this.offsetY + 50; // 稍微下沉一点 }) } else { // 回弹复位动画 animateTo({ duration: 300, curve: curves.springMotion() }, () => { this.offsetX = 0; this.offsetY = 0; this.rotateAngle = 0; }) } }) : null ) } // 计算缩放比例:顶层1.0,第二层0.95,第三层0.9... getScale(index: number): ScaleOptions { if (index === 0) return { x: 1, y: 1 }; // 简单的层级缩放逻辑 const scale = 1 - (index * 0.05); return { x: scale, y: scale }; } // 计算透明度 getOpacity(index: number): number { return 1 - (index * 0.1); // 越往后越透明 } // 计算位移:顶层受手势控制,后层固定偏移 getTranslate(index: number): TranslateOptions { if (index === 0) { return { x: this.offsetX, y: this.offsetY }; } // 后面的卡片向下偏移,形成堆叠感 return { x: 0, y: index * 20 }; }}1.5 总结· 问题与痛点:高频信息浏览场景下,传统列表缺乏交互快感与筛选效率。· 技术要点:Stack + zIndex :构建多维视觉空间。PanGesture :实现精准的触控跟随。Data Driven UI :通过改变数据源(Array.shift)配合 animateTo 自动触发布局更新与过渡动画。· 实现效果:高性能的层叠卡片滑动切换效果。· 适用场景:求职招聘(岗位卡片)、社交应用(好友推荐)、电商活动(商品秒杀)、新闻资讯(卡片式阅读)。
-
1.1 问题说明在新闻资讯、电子书、漫画等沉浸式阅读类应用中,用户往往需要长时间保持阅读状态。频繁的屏幕滑动(Swipe)翻页操作容易导致手指疲劳,且手指在屏幕上的移动会短暂遮挡阅读内容,破坏沉浸感。特别是在单手握持设备的场景下(如在公交、地铁通勤途中),拇指难以覆盖整个屏幕区域,用户急需一种无需触屏、仅靠物理按键即可完成翻页的便捷交互方式。1.2 原因分析交互局限性纯触屏交互在单手操作或移动场景下稳定性较差,容易误触或操作不到位。物理反馈缺失触屏滑动缺乏明确的触觉反馈,用户无法盲操作。系统默认行为音量键默认用于调节系统媒体音量。若不进行特定的事件拦截与处理,用户按键时只会改变音量,无法触发应用内的业务逻辑。1.3 解决思路· 输入事件拦截 :利用 HarmonyOS 的 inputConsumer (输入消费)能力,在阅读界面定向监听物理按键事件。· 键值映射 :识别 KEYCODE_VOLUME_UP (音量+)和 KEYCODE_VOLUME_DOWN (音量-)键值,将其分别映射为阅读控制器的 flipPage(false) (上一页)和 flipPage(true) (下一页)指令。· 生命周期管理 :严格控制监听器的注册与注销。仅在阅读页显示( onPageShow )时接管音量键,在页面隐藏( onPageHide )时释放控制权,确保不影响系统其他场景的音量调节。1.4 解决方案权限配置 :通常不需要额外申请敏感权限,但需确保应用处于前台活跃状态。系统兼容性 :不同 ROM 或系统版本对音量键的拦截策略可能不同。如果系统强制优先响应音量调节,可能需要检查是否有“独占模式”或相关配置。API 版本 :本示例基于 HarmonyOS SDK 的 InputKit 能力,需确认当前开发环境 SDK 版本支持 inputConsumer 接口。 import { inputConsumer, KeyCode, KeyOptions } from '@kit.InputKit';import { promptAction } from '@kit.ArkUI'; // 假设的阅读器控制器接口interface ReaderController { flipPage(next: boolean): void;} @Entry@Componentstruct ReadingPage { // 模拟阅读器控制器 private readerComponentController: ReaderController = { flipPage: (next: boolean) => { const direction = next ? '下一页' : '上一页'; console.info(`翻页操作: ${direction}`); promptAction.showToast({ message: `执行翻页: ${direction}` }); // 实际业务中这里调用 Swiper 或 List 的 scroll/showNext 接口 } }; // 页面显示时注册监听 onPageShow(): void { this.registerVolumeKey(); } // 页面隐藏时取消监听,恢复系统音量功能 onPageHide(): void { this.unregisterVolumeKey(); } // 注册音量键监听 registerVolumeKey() { // 1. 配置音量+(上一页) let upOption: inputConsumer.KeyOptions = { preKeys: [], // 无组合键 finalKey: KeyCode.KEYCODE_VOLUME_UP, isFinalKeyDown: true, // 按下时触发 finalKeyDownDuration: 0 // 无需长按 }; // 2. 配置音量-(下一页) let downOption: inputConsumer.KeyOptions = { preKeys: [], finalKey: KeyCode.KEYCODE_VOLUME_DOWN, isFinalKeyDown: true, finalKeyDownDuration: 0 }; // 3. 绑定监听回调 try { inputConsumer.on('key', upOption, (keyOption: inputConsumer.KeyOptions) => { // 阻止系统默认音量变化(视具体API版本行为而定,部分场景可能需要额外配置屏蔽系统行为) this.readerComponentController.flipPage(false); }); inputConsumer.on('key', downOption, (keyOption: inputConsumer.KeyOptions) => { this.readerComponentController.flipPage(true); }); console.info('音量键监听注册成功'); } catch (error) { console.error(`注册失败: ${JSON.stringify(error)}`); } } // 注销监听 unregisterVolumeKey() { try { // 注销所有该类型的键值监听,或传入具体的 KeyOptions let upOption: inputConsumer.KeyOptions = { preKeys: [], finalKey: KeyCode.KEYCODE_VOLUME_UP, isFinalKeyDown: true, finalKeyDownDuration: 0 }; let downOption: inputConsumer.KeyOptions = { preKeys: [], finalKey: KeyCode.KEYCODE_VOLUME_DOWN, isFinalKeyDown: true, finalKeyDownDuration: 0 }; inputConsumer.off('key', upOption); inputConsumer.off('key', downOption); console.info('音量键监听已注销'); } catch (error) { console.error(`注销失败: ${JSON.stringify(error)}`); } } build() { Column() { Text('阅读器模拟界面') .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 50, bottom: 20 }) Text('请尝试按下手机音量键进行翻页') .fontSize(16) .fontColor(Color.Gray) // 模拟书籍内容区域 Container() { Text('这里是正文内容... \n\n沉浸式阅读体验不仅依赖于视觉设计,交互的便捷性同样至关重要。通过复用音量实体键,我们为用户提供了“盲操作”的可能,这在拥挤的地铁、晃动的车厢中显得尤为珍贵。') .fontSize(18) .lineHeight(30) .padding(20) } .width('90%') .height('60%') .backgroundColor('#F0F0F0') .borderRadius(12) .margin({ top: 20 }) } .width('100%') .height('100%') }}1.5 总结· 问题与痛点:单手阅读累、触屏遮挡视线、缺乏物理反馈。· 技术要点:使用 inputConsumer 订阅 KEYCODE_VOLUME_UP/DOWN 事件,结合生命周期管理实现按键复用。· 实现效果:用户可通过点击音量实体键流畅翻页,提升了单手操作的便捷性与阅读沉浸感。· 适用场景:新闻客户端(长文阅读)、小说阅读器(连续翻页)、漫画应用(图片切换)、PPT 演示工具(远程翻页)。
-
1.1 问题说明鸿蒙应用中进行多音频合成时,存在三大核心问题:一是多音频文件(最多 5 个)同时处理易导致主线程阻塞,引发 UI 卡顿甚至无响应;二是音频格式(MP3、AAC、M4A、WAV 等)兼容性不足,不同格式合成易失败;三是缺乏进度反馈,用户无法知晓合成状态,且异常场景下无资源清理机制,易残留无效文件。 1.2 原因分析主线程承载过重:音频合成属于 CPU 密集型操作,未拆分至独立线程,与 UI 渲染、用户交互抢占主线程资源,导致阻塞。编码器未适配多格式:不同音频格式对应的编码标准不同,固定编码参数无法兼容所有格式,易出现格式解析失败或合成后音频损坏。路径处理不规范:音频文件路径可能包含空格、特殊字符,未做转义处理会导致 FFmpeg 命令解析失败。缺乏异步调度与反馈机制:未实现异步任务管理,且无进度监控和异常回调,用户无法感知合成进度,异常时也无法及时清理临时资源。 1.3 解决思路线程分离:采用 Worker 多线程架构,将音频合成任务剥离至独立线程,避免占用主线程。动态适配:构建自适应编码器选择逻辑,根据输出音频格式动态匹配对应的编码参数。规范命令构建:对文件路径进行转义处理,动态生成 FFmpeg 多输入参数和滤镜配置,确保命令可执行。全流程管控:增加进度监控、异常回调和资源清理机制,提升用户体验并避免资源残留。 1.4 解决方案 动态输入参数与滤镜配置针对多音频输入和格式兼容问题,通过数组映射动态生成 FFmpeg 命令参数,结合 concat 滤镜实现无缝拼接:typescript运行// 动态构建输入参数(支持最多5个音频文件)const filesToMerge = this.audioFiles.slice(0, 5); // 限制最大输入数量,避免过载const fileCount = filesToMerge.length;// 路径用双引号包裹,处理空格和特殊字符const inputParams = filesToMerge.map(path => `-i "${path}"`).join(' ');// 配置concat滤镜:禁用视频流,启用单音频流拼接const concatFilter = `concat=n=${fileCount}:v=0:a=1`;技术要点:通过切片限制最大输入文件数(可按需调整),双引号包裹路径避免解析错误,concat 滤镜动态适配输入文件数量,确保多音频按时间顺序无缝合成。 自适应编码器选择根据输出音频格式动态匹配编码参数,保障格式兼容性:typescript运行const ext = this.outPath.substring(this.outPath.lastIndexOf('.')).toLowerCase();let codecParams = '';switch (ext) { case '.mp3': codecParams = '-c:a libmp3lame -b:a 128k'; // 兼容性最佳,码率128kbps break; case '.aac': case '.m4a': codecParams = '-c:a aac -b:a 128k'; // 高压缩比,适配主流音频格式 break; case '.wav': codecParams = '-c:a pcm_s16le'; // 无损编码,适合后期处理 break; default: codecParams = '-c:a libmp3lame -b:a 128k'; // 默认降级为MP3,避免失败}编码策略:针对不同格式选择最优编码器,未识别格式自动降级,最大化合成成功率。 完整命令组装与异步执行整合参数生成完整 FFmpeg 命令,通过 Worker 线程异步执行,避免阻塞主线程:typescript运行// 组装完整命令:-y自动覆盖输出文件,-progress记录进度日志const cmd = `ffmpeg -y ${inputParams} -filter_complex "${concatFilter}" ${codecParams} "${this.outPath}" -progress ${this.logPath}`;// Worker线程中执行命令(独立线程,不影响UI)onStart() { const workerId = this.workerId; let callBack: ICallBack = { callBackResult(code: number) { if (code === 0) { WorkersController.noticeWorkerTaskEnd(workerId); // 任务成功结束 } else { this.deleteTempFile(); // 失败时清理资源 this.release(); // 释放线程 } } }; MP4Parser.ffmpegCmd(cmd, callBack); // 调用@ohos/mp4parser库执行FFmpeg命令} 资源清理与异常处理针对任务取消、失败等场景,实现全流程资源清理,避免无效文件残留:typescript运行// 任务取消时清理资源onCancel() { this.deleteTempFile(); // 删除进度日志 FileUtil.deleteFile(this.outPath); // 删除未完成的输出文件 this.release(); // 释放Worker线程资源}// 删除临时进度文件deleteTempFile() { FileUtil.deleteFile(this.logPath);} 1.5 总结问题痛点:多音频合成阻塞主线程、格式兼容差、无进度反馈、资源残留。关键技术:Worker 多线程异步调度、FFmpeg 动态命令构建、自适应编码器适配、全流程资源清理。技术亮点:通过路径转义、格式降级保障兼容性,线程分离解决 UI 阻塞,进度日志与回调提升用户体验。实施效果:支持 4 种主流音频格式合成,无主线程阻塞,异常场景资源无残留,已在 HarmonyOS 5.0.5 及以上版本、手机 / 折叠屏 / 平板等设备验证通过。
-
公司电脑上是有企业的绿盾加密的,正常像VS Code打开项目是可以正常阅读编辑,但是用CodeArts Agent打开项目全是二进制乱码,如果在VS Code安装插件版有Bug,提问一次后,要么无限制回复,要么第二次提问就废了,回复一堆带有html文本的东西,如何解决CodeArts Agent在绿盾环境下使用的问题?
yd_230012403
发表于2026-03-09 10:48:11
2026-03-09 10:48:11
最后回复
CodeArts小助手-蚂蚁
2026-03-09 14:20:40
46 1 -
一、问题背景在鸿蒙应用开发中,经常会遇到需要对指定 UI 组件进行截图的场景(如生成分享卡片、保存页面关键内容等)。开发者在实现过程中容易遇到组件未挂载、图片加载未完成、截图超时、备用方案缺失等问题,导致截图功能不稳定甚至失效。本文基于鸿蒙@kit.ImageKit提供完整的截图解决方案,包含主备两套截图逻辑、错误码精准处理、资源释放等核心功能,确保截图功能健壮可用。二、核心技术依赖本次方案基于鸿蒙的两个核心 Kit,无需额外引入第三方依赖:@kit.ImageKit:提供PixelMap(截图结果承载)、ComponentSnapshot(组件截图核心类)相关能力,是截图功能的基础。@kit.BasicServicesKit:提供BusinessError,用于精准捕获和处理截图过程中的业务错误。三、完整实现代码 import { image } from "@kit.ImageKit";import { BusinessError } from '@kit.BasicServicesKit';@Componentexport struct ImagePage { @State screenshotResult: image.PixelMap | undefined = undefined; @State screenshotStatus: string = '等待截图'; // 直接在组件中管理截图逻辑,避免工具类问题 private uiContext: UIContext = this.getUIContext(); @Builder TargetComponent() { Column() { Text('这是要截图的内容') .fontSize(20) .fontColor(Color.Black) .margin(10) Image($r('app.media.background')) .width(100) .height(100) .syncLoad(true) // 关键:强制同步加载图片 .margin(10) } .padding(20) .backgroundColor(Color.White) // .border({ width: 2, color: Color.Gray }) .alignItems(HorizontalAlign.Center) } /** * 截图方法 - 直接使用组件内的 UIContext */ async capture(builder: () => void): Promise<boolean> { try { console.info('开始截图流程...'); // 1. 获取 ComponentSnapshot 对象 const componentSnapshot = this.uiContext.getComponentSnapshot(); if (!componentSnapshot) { console.error('无法获取 ComponentSnapshot 对象'); return false; } // 2. 检查 createFromBuilder 方法是否存在 if (typeof componentSnapshot.createFromBuilder !== 'function') { console.error('createFromBuilder 方法不存在,尝试使用其他方法'); return await this.alternativeCaptureMethod(); } console.info('使用 createFromBuilder 方法截图...'); // 3. 执行截图 const pixelMap = await componentSnapshot.createFromBuilder( builder, 800, // 增加延迟时间确保组件构建完成 true, // 检查图片状态 { scale: 0.8, waitUntilRenderFinished: true } ); // 4. 验证结果 if (!pixelMap) { console.error('截图返回的 PixelMap 为 undefined'); return false; } this.screenshotResult = pixelMap; console.info(`截图成功,像素字节数: ${pixelMap.getPixelBytesNumber()}`); return true; } catch (error) { console.error('截图失败详细错误:'); console.error('错误消息:', error.message); console.error('错误代码:', error.code); console.error('错误名称:', error.name); // 根据错误代码提供具体建议 this.handleSpecificError(error); return false; } } /** * 备用截图方法 - 使用 get 方法截图已挂载的组件 */ async alternativeCaptureMethod(): Promise<boolean> { try { console.info('尝试备用截图方法...'); const componentSnapshot = this.uiContext.getComponentSnapshot(); // 给目标组件添加 ID const pixelMap = await componentSnapshot.get('targetComponent', { scale: 0.8, waitUntilRenderFinished: true }); this.screenshotResult = pixelMap; return true; } catch (error) { console.error('备用方法也失败:', error); return false; } } /** * 处理特定错误 */ handleSpecificError(error:BusinessError): void { if (error.code === 100001) { console.error('错误 100001: 组件ID无效或组件未挂载'); } else if (error.code === 160001) { console.error('错误 160001: 图片加载未完成,建议增加延迟或设置 syncLoad=true'); } else if (error.code === 160002) { console.error('错误 160002: 截图超时,建议减少缩放比例或组件复杂度'); } else if (error.code === 401) { console.error('错误 401: 参数错误,检查参数类型和格式'); } } build() { Column() { Text('截图功能演示') .fontSize(24) .fontWeight(FontWeight.Bold) .margin(20) Text(`截图状态: ${this.screenshotStatus}`) .fontSize(16) .fontColor(this.screenshotStatus.includes('成功') ? Color.Green : Color.Red) .margin(10) Button('开始截图') .width(200) .height(50) .fontSize(18) .onClick(async () => { this.screenshotStatus = '截图进行中...'; // 直接调用组件内的方法 const success = await this.capture((): void => this.TargetComponent()); if (success) { this.screenshotStatus = '截图成功!'; } else { this.screenshotStatus = '截图失败,请重试'; } }) .margin(20) // 显示截图结果 if (this.screenshotResult) { Text('截图预览:') .fontSize(18) .margin(10) Image(this.screenshotResult) .width(300) .height(200) .border({ width: 2, color: Color.Blue }) .margin(10) Button('释放资源') .onClick(() => { if (this.screenshotResult) { this.screenshotResult.release(); this.screenshotResult = undefined; this.screenshotStatus = '资源已释放'; } }) .margin(10) } // 调试信息区域 Text('调试信息:') .fontSize(14) .fontColor(Color.Gray) .margin({ top: 20 }) Text(`UIContext: ${this.uiContext ? '已初始化' : '未初始化'}`) .fontSize(12) .fontColor(Color.Gray) Text(`截图结果: ${this.screenshotResult ? '有数据' : '无数据'}`) .fontSize(12) .fontColor(Color.Gray) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .backgroundColor(Color.White) }} 四、关键技术点解析1. 核心截图逻辑(主方案:createFromBuilder)基于组件内UIContext获取ComponentSnapshot截图实例,避免跨组件传递上下文导致的问题。直接传入@Builder构建的目标组件,无需组件提前挂载,灵活性更高。配置关键参数:800ms延迟确保组件构建、syncLoad=true强制图片同步加载、waitUntilRenderFinished=true等待渲染完成,大幅提升截图成功率。2. 备用截图逻辑(降级方案:componentSnapshot.get)当主方案createFromBuilder方法不可用(兼容性场景)时,自动切换至备用方案。针对已挂载的组件,通过组件 ID(targetComponent)进行截图,作为主方案的兜底保障,提升功能健壮性。3. 精准错误处理针对鸿蒙截图常见错误码(100001、160001、160002、401)进行分类处理,明确错误原因和修复建议。捕获BusinessError详细信息(消息、代码、名称),方便开发者调试排查问题。4. 内存优化(PixelMap.release())PixelMap对象会占用一定内存,截图完成后不再使用时,需调用release()方法释放资源,避免内存泄漏,尤其在频繁截图场景下至关重要。五、常见问题解决方案截图空白 / 图片未显示:给Image组件添加syncLoad=true,强制图片同步加载,避免截图时图片还在异步加载中。截图超时(错误 160002):减少组件复杂度(拆分复杂组件)、降低缩放比例(如从1.0调整为0.8)、延长截图延迟时间。组件 ID 无效(错误 100001):确保备用方案中组件 ID 与目标组件配置一致,且组件已完成挂载(避免在组件初始化前调用截图)。ComponentSnapshot获取失败:确保在组件挂载后获取UIContext,避免在组件初始化阶段调用截图逻辑。六、总结本方案实现了鸿蒙应用中 UI 组件截图的完整功能,具备以下优势:主备两套逻辑,兼顾灵活性和兼容性,确保截图功能稳定可用。关键参数优化,大幅提升截图成功率,解决常见的空白、超时问题。精准错误处理和内存优化,方便调试且避免资源泄漏。代码结构清晰,可直接复用,支持快速集成到各类鸿蒙应用中。适用于鸿蒙 ArkTS(Stage 模型)应用开发,支持截图预览、资源释放、状态提示等完整流程,满足绝大多数场景下的组件截图需求。
-
在鸿蒙分布式应用开发中,需要实现设备间的自动发现、认证和连接,以构建跨设备协同体验。开发者常遇到以下具体问题:发现机制分散:蓝牙、Wi-Fi P2P、局域网发现多种技术并存协议栈不统一:不同设备支持的连接协议有差异安全机制严格:分布式安全要求导致连接流程复杂解决方案环境配置# 1. 确保开发环境配置正确# 检查DevEco Studio版本# 2. 创建鸿蒙项目# 选择Application -> Empty Ability# 模型选择:Stage# 开发语言:ArkTS# API版本:9+ 1.1 设备管理封装类// DeviceManager.ts - 鸿蒙设备管理器import deviceManager from '@ohos.distributedDeviceManager';import { BusinessError } from '@ohos.base';import { common } from '@kit.AbilityKit';export class HarmonyDeviceManager { private deviceDiscovery: deviceManager.DeviceDiscovery | null = null; private deviceList: deviceManager.DeviceBasicInfo[] = []; private authCallback: deviceManager.AuthCallback | null = null; private connectionCallback: deviceManager.DeviceConnectCallback | null = null; // 初始化设备管理器 async initDeviceManager(context: common.UIAbilityContext): Promise<void> { try { console.info('[DeviceManager] Initializing device manager...'); // 创建设备管理器实例 const manager = deviceManager.createDeviceManager(context.bundleName, { bundleName: context.bundleName, callback: (action: deviceManager.DeviceStateChangeAction, device: deviceManager.DeviceBasicInfo) => { this.handleDeviceStateChange(action, device); } }); // 设置连接状态回调 this.setupConnectionCallback(); console.info('[DeviceManager] Initialization completed'); } catch (error) { console.error('[DeviceManager] Initialization failed:', error); } } // 开始发现设备 async startDiscovery(options?: deviceManager.DiscoveryOptions): Promise<void> { try { const defaultOptions: deviceManager.DiscoveryOptions = { discoveryMode: deviceManager.DiscoveryMode.DISCOVERY_MODE_ACTIVE, medium: deviceManager.ExchangeMedium.COAP, freq: deviceManager.ExchangeFreq.LOW, isActiveDiscover: true, ...options }; this.deviceDiscovery = await deviceManager.startDeviceDiscovery(defaultOptions); // 监听设备发现事件 this.deviceDiscovery.on('discoverSuccess', (device: deviceManager.DeviceBasicInfo) => { this.onDeviceDiscovered(device); }); this.deviceDiscovery.on('discoverFail', (errorCode: number) => { console.error(`[DeviceManager] Discovery failed: ${errorCode}`); }); } catch (error) { console.error('[DeviceManager] Start discovery failed:', error); } } // 设备发现回调 private onDeviceDiscovered(device: deviceManager.DeviceBasicInfo): void { // 去重处理 const existingIndex = this.deviceList.findIndex(d => d.deviceId === device.deviceId); if (existingIndex === -1) { this.deviceList.push(device); console.info(`[DeviceManager] New device discovered: ${device.deviceName} (${device.deviceId})`); // 触发设备更新事件 this.emitDeviceListUpdated(); } } // 连接设备 async connectDevice(deviceId: string, authParam?: deviceManager.AuthParam): Promise<boolean> { try { console.info(`[DeviceManager] Connecting to device: ${deviceId}`); const authParam: deviceManager.AuthParam = { authType: deviceManager.AuthType.PIN_CODE, appIcon: '', appThumbnail: '', ...authParam }; // 发起认证请求 await deviceManager.authenticateDevice(authParam, { onSuccess: (data: { deviceId: string; pinCode?: string }) => { console.info(`[DeviceManager] Authentication success: ${data.deviceId}`); this.onAuthSuccess(deviceId); }, onError: (error: BusinessError) => { console.error(`[DeviceManager] Authentication failed: ${JSON.stringify(error)}`); } }); return true; } catch (error) { console.error('[DeviceManager] Connect device failed:', error); return false; } } // 认证成功处理 private onAuthSuccess(deviceId: string): void { // 建立连接 deviceManager.connectDevice( deviceId, deviceManager.ConnectType.TYPE_WIFI_P2P, this.connectionCallback! ).then(() => { console.info(`[DeviceManager] Device ${deviceId} connected successfully`); }).catch((error: BusinessError) => { console.error(`[DeviceManager] Connect failed: ${JSON.stringify(error)}`); }); } // 设置连接回调 private setupConnectionCallback(): void { this.connectionCallback = { onConnect: (deviceInfo: deviceManager.DeviceBasicInfo) => { console.info(`[DeviceManager] Device connected: ${deviceInfo.deviceName}`); this.emitDeviceConnected(deviceInfo); }, onDisconnect: (deviceInfo: deviceManager.DeviceBasicInfo) => { console.info(`[DeviceManager] Device disconnected: ${deviceInfo.deviceName}`); this.emitDeviceDisconnected(deviceInfo); } }; } // 设备状态变更处理 private handleDeviceStateChange( action: deviceManager.DeviceStateChangeAction, device: deviceManager.DeviceBasicInfo ): void { switch (action) { case deviceManager.DeviceStateChangeAction.ONLINE: console.info(`[DeviceManager] Device online: ${device.deviceName}`); break; case deviceManager.DeviceStateChangeAction.OFFLINE: case deviceManager.DeviceStateChangeAction.READY_OFFLINE: console.info(`[DeviceManager] Device offline: ${device.deviceName}`); break; } } // 获取设备列表 getDevices(): deviceManager.DeviceBasicInfo[] { return [...this.deviceList]; } // 停止发现 async stopDiscovery(): Promise<void> { if (this.deviceDiscovery) { await this.deviceDiscovery.stop(); this.deviceDiscovery = null; } } // 清理资源 release(): void { this.stopDiscovery(); this.deviceList = []; } // 事件发射器(简化版) private emitDeviceListUpdated(): void { // 实际实现中可使用EventEmitter } private emitDeviceConnected(device: deviceManager.DeviceBasicInfo): void { // 实际实现中可使用EventEmitter } private emitDeviceDisconnected(device: deviceManager.DeviceBasicInfo): void { // 实际实现中可使用EventEmitter }}1.2 UI组件封装// DeviceListComponent.ets - 设备列表组件@Componentexport struct DeviceListComponent { @State deviceList: Array<DeviceItem> = []; private deviceManager: HarmonyDeviceManager = new HarmonyDeviceManager(); aboutToAppear(): void { this.initDeviceDiscovery(); } // 初始化设备发现 async initDeviceDiscovery(): Promise<void> { // 请求权限 await this.requestPermissions(); // 初始化设备管理器 await this.deviceManager.initDeviceManager(getContext(this) as common.UIAbilityContext); // 开始发现设备 await this.deviceManager.startDiscovery(); // 定时刷新设备列表 setInterval(() => { this.deviceList = this.deviceManager.getDevices().map(device => ({ id: device.deviceId, name: device.deviceName || 'Unknown Device', type: this.getDeviceType(device.deviceType), isConnected: false // 实际应从设备管理器获取连接状态 })); }, 2000); } // 请求必要权限 async requestPermissions(): Promise<void> { const permissions: Array<string> = [ 'ohos.permission.DISTRIBUTED_DATASYNC', 'ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE', 'ohos.permission.GET_NETWORK_INFO' ]; for (const permission of permissions) { try { const result = await abilityAccessCtrl.requestPermissionsFromUser( getContext(this) as common.UIAbilityContext, [permission] ); console.info(`[DeviceList] Permission ${permission} granted: ${result.authResults[0] === 0}`); } catch (error) { console.error(`[DeviceList] Permission request failed: ${error}`); } } } // 连接设备 async connectToDevice(deviceId: string): Promise<void> { const success = await this.deviceManager.connectDevice(deviceId); if (success) { promptAction.showToast({ message: '设备连接成功' }); } else { promptAction.showToast({ message: '设备连接失败' }); } } // 获取设备类型图标 getDeviceType(deviceType: number): string { switch (deviceType) { case 0x00: return 'phone'; // 手机 case 0x01: return 'tablet'; // 平板 case 0x02: return 'tv'; // 智慧屏 case 0x03: return 'watch'; // 手表 default: return 'device'; } } build() { Column() { // 标题 Text('附近设备') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 20 }) // 设备列表 List({ space: 10 }) { ForEach(this.deviceList, (device: DeviceItem) => { ListItem() { DeviceItemComponent({ device: device, onConnect: (id: string) => { this.connectToDevice(id); }}) } }) } .layoutWeight(1) // 操作按钮 Row() { Button('重新扫描') .onClick(() => { this.deviceManager.stopDiscovery(); this.deviceManager.startDiscovery(); }) .margin({ right: 10 }) Button('停止发现') .onClick(() => { this.deviceManager.stopDiscovery(); }) } .justifyContent(FlexAlign.Center) .margin({ top: 20, bottom: 20 }) } }}// 设备项组件@Componentstruct DeviceItemComponent { private device: DeviceItem = { id: '', name: '', type: '', isConnected: false }; private onConnect?: (deviceId: string) => void; build() { Row() { // 设备图标 Image($r(`app.media.ic_device_${this.device.type}`)) .width(40) .height(40) .margin({ right: 15 }) // 设备信息 Column() { Text(this.device.name) .fontSize(16) .fontWeight(FontWeight.Medium) Text(`设备ID: ${this.device.id.substring(0, 8)}...`) .fontSize(12) .fontColor(Color.Gray) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) // 连接按钮 Button(this.device.isConnected ? '已连接' : '连接') .enabled(!this.device.isConnected) .onClick(() => { if (this.onConnect) { this.onConnect(this.device.id); } }) } .padding(15) .backgroundColor(Color.White) .borderRadius(8) .shadow({ radius: 4, color: Color.Black, offsetX: 0, offsetY: 2 }) }} 1.3 权限配置文件// module.json5{ "module": { "requestPermissions": [ { "name": "ohos.permission.DISTRIBUTED_DATASYNC", "reason": "需要同步数据到其他设备", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE", "reason": "需要监听设备状态变化", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.GET_NETWORK_INFO", "reason": "需要获取网络信息进行设备发现", "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ], "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "permissions": [ "ohos.permission.DISTRIBUTED_DATASYNC", "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE" ] } ] }} 步骤1:添加依赖// oh-package.json5{ "dependencies": { "@ohos/distributedDeviceManager": "file:../feature/distributed_device_manager" }} 步骤2:配置设备能力// module.json5{ "module": { "name": "entry", "type": "entry", "deviceTypes": ["phone", "tablet", "tv", "wearable"], "distributedNotificationEnabled": true, "distributedPermissions": { "com.example.myapp": { "data": { "access": ["read", "write"], "uri": "dataability:///com.example.myapp.DataAbility" } } } }} 步骤3:实现设备发现服务// DeviceDiscoveryService.tsexport class DeviceDiscoveryService { private static instance: DeviceDiscoveryService; private discoveryCallbacks: Array<(devices: DeviceBasicInfo[]) => void> = []; static getInstance(): DeviceDiscoveryService { if (!DeviceDiscoveryService.instance) { DeviceDiscoveryService.instance = new DeviceDiscoveryService(); } return DeviceDiscoveryService.instance; } // 统一发现接口 async discoverNearbyDevices(options: DiscoveryOptions = {}): Promise<DeviceBasicInfo[]> { const devices: DeviceBasicInfo[] = []; // 多协议并行发现 await Promise.all([ this.discoverViaBluetooth(devices, options), this.discoverViaWiFi(devices, options), this.discoverViaCoap(devices, options) ]); // 去重和排序 return this.deduplicateAndSortDevices(devices); } private async discoverViaBluetooth( devices: DeviceBasicInfo[], options: DiscoveryOptions ): Promise<void> { // 蓝牙发现实现 } private async discoverViaWiFi( devices: DeviceBasicInfo[], options: DiscoveryOptions ): Promise<void> { // Wi-Fi发现实现 } private async discoverViaCoap( devices: DeviceBasicInfo[], options: DiscoveryOptions ): Promise<void> { // CoAP发现实现 }} 步骤5:实现连接管理// ConnectionManager.tsexport class ConnectionManager { private connections: Map<string, DeviceConnection> = new Map(); // 建立连接 async establishConnection( deviceId: string, options: ConnectionOptions ): Promise<DeviceConnection> { const connection: DeviceConnection = { deviceId, status: 'connecting', timestamp: Date.now(), retryCount: 0 }; this.connections.set(deviceId, connection); try { // 1. 设备认证 await this.authenticateDevice(deviceId, options.authType); // 2. 建立传输通道 const channel = await this.createChannel(deviceId, options.channelType); // 3. 启动心跳检测 this.startHeartbeat(deviceId); connection.status = 'connected'; connection.channel = channel; console.info(`[ConnectionManager] Device ${deviceId} connected successfully`); return connection; } catch (error) { connection.status = 'failed'; connection.error = error as Error; // 重试逻辑 if (connection.retryCount < options.maxRetries || 3) { connection.retryCount++; return this.establishConnection(deviceId, options); } throw error; } }} 测试环境:2台华为P60,HarmonyOS 4.0测试场景:设备发现与连接 可复用组件清单HarmonyDeviceManager - 核心设备管理类DeviceListComponent - 设备列表UI组件ConnectionManager - 连接状态管理DeviceDiscoveryService - 统一发现服务PermissionHelper - 权限管理工具示例配置文件 - 权限、能力配置模板 最佳实践总结统一入口:封装所有设备操作到一个管理器事件驱动:使用观察者模式监听设备状态变化错误处理:统一的错误处理重试机制权限管理:按需请求,优雅降级状态管理:使用状态机管理连接生命周期多协议支持:自动选择最优发现协议资源释放:合理释放不使用的资源
-
按照要求实现了接口,但都是0分。
-
35期提交作品出现这个是为什么哇
-
安装时提示“这个程序不支持该版本的计算机运行。”是因为我的电脑win7版本、64位操作系统吗?
-
运行模拟器就报这个错误
-
您好,请问2025年华为软挑初赛证书怎么获取?
-
【问题来源】【必填】 星网【问题简要】【必填】isales接口【问题类别】【必填】 isales【AICC解决方案版本】【必填】 aicc.22.200.0【期望解决时间】【选填】尽快【问题现象描述】【必填】 新建活动时全局 policyType=0的时候的结果类型resultRetry按文档要求上送json串一直不对 【日志或错误截图】【可选】
-
一、 关键技术难点总结1.1 问题说明从相册选择图片后,获取不到照片的位置信息。1.2 原因分析图片的经纬度信息存储在EXIF里,对应的key是:GPS_LONGITUDE和GPS_LATITUDE。图片工具当前主要提供图片EXIF信息的读取与编辑能力。EXIF(Exchangeable image file format)是专门为数码相机的照片设定的文件格式,可以记录数码照片的属性信息和拍摄数据。当前仅支持JPEG格式图片。1.3 解决思路在图库等应用中,需要查看或修改数码照片的EXIF信息。由于摄像机的手动镜头的参数无法自动写入到EXIF信息中或者因为相机断电等原因经常会导致拍摄时间出错,这时候就需要手动修改错误的EXIF数据,即可使用本功能。1.4 解决方案1、鸿蒙中图片怎么读取exif信息获取图片信息,需要先将图库图片拷贝到沙箱路径中。当需要调用图片信息时,使用PhotoViewPicker选择指定的图片资源,文件选择成功后,返回PhotoSelectResult结果集。将图库图片复制到沙箱中的参考代码如下:async photoPick() {try {let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;PhotoSelectOptions.maxSelectNumber = 5;let photoPicker = new photoAccessHelper.PhotoViewPicker();photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ’ +JSON.stringify(PhotoSelectResult));let file1 = fs.openSync(PhotoSelectResult.photoUris[0])fs.copyFileSync(file1.fd, data/storage/el2/base/haps/entry/files/${file1.name})let file2 = fs.openSync(data/storage/el2/base/haps/entry/files/${file1.name}, fs.OpenMode.READ_WRITE)console.log(file fd ==> ${file2.fd} | file path ==> ${file2.path})this.filePath = file2.path}).catch((err: BusinessError) => {console.error('PhotoViewPicker.select failed with err: ’ + JSON.stringify(err));});} catch (error) {let err: BusinessError = error as BusinessError;console.error('PhotoViewPicker failed with err: ’ + JSON.stringify(err));}}2、将图片拷贝到沙箱路径中后,使用ImageSource对图片进行解码,再通过getImageInfo获取图片信息或getImageProperty获取指定的图片属性值。注意:getImageProperty仅支持JPEG、PNG和HEIF12+(不同硬件设备支持情况不同)文件,且需要包含exif信息,获取的属性值必须是图片属性中存在的,不存在的话则会返回ImagePropertyOptions中的设定值defaultValue。参考代码如下:private async getImageCoordinates() {console.log(‘输出:filePath’,this.filePath)let imageSource = image.createImageSource(this.filePath);const keys = [image.PropertyKey.GPS_LATITUDE,image.PropertyKey.GPS_LONGITUDE,image.PropertyKey.GPS_LATITUDE_REF,image.PropertyKey.GPS_LONGITUDE_REF]imageSource.getImageProperties(keys).then((data) => {console.info('批量获取图片中的指定属性键的值success: ',JSON.stringify(data));// 提取GPS数据const latitudeDms = data[image.PropertyKey.GPS_LATITUDE] as string;const longitudeDms = data[image.PropertyKey.GPS_LONGITUDE] as string;const latitudeRef = data[image.PropertyKey.GPS_LATITUDE_REF] as string;const longitudeRef = data[image.PropertyKey.GPS_LONGITUDE_REF] as string; // 转换为十进制 const latitude = this.dmsToDecimal(latitudeDms, latitudeRef); const longitude = this.dmsToDecimal(longitudeDms, longitudeRef); // 输出结果 console.log('原始GPS数据:'); console.log('纬度:', latitudeDms, latitudeRef); console.log('经度:', longitudeDms, longitudeRef); console.log('转换后的十进制坐标:'); console.log('纬度:', latitude); console.log('经度:', longitude); console.log('坐标格式:', `${latitude}, ${longitude}`); }).catch((err: BusinessError) => { console.error('批量获取图片中的指定属性键的值error: ', JSON.stringify(err)); }); // 获取指定序号的图片信息 let imageInfo = imageSource.getImageInfoSync(0); console.log('获取指定序号的图片信息', JSON.stringify(imageInfo)) if (imageInfo == undefined) { console.error('Failed to obtain the image information.'); } else { console.info('Succeeded in obtaining the image information.', JSON.stringify(imageInfo)); }}// 将度分秒格式的GPS坐标转换为十进制格式dmsToDecimal(dms: string, ref: string): number {// 去除空格并按逗号分割const parts = dms.replace(/\s+/g, ‘’).split(‘,’);if (parts.length !== 3) return NaN;// 解析度、分、秒const deg = parseFloat(parts[0]);const min = parseFloat(parts[1]);const sec = parseFloat(parts[2]);// 计算十进制值let decimal = deg + min / 60 + sec / 3600;// 根据参考方向调整正负if (ref === ‘S’ || ref === ‘W’) {decimal = -decimal;}return decimal;}3、参考链接:获取图片经纬度信息:https://developer.huawei.com/consumer/cn/forum/topic/0208180370773931585?fid=0109140870620153026,https://developer.huawei.com/consumer/cn/forum/topic/0202178283677576203
-
1.问题说明:播放视频时无法自动连播,声音会重复2.原因分析:没有监听播放结束状态和释放视频资源3.解决思路:(1) Swiper 滑动切换机制(2) 视频播放器生命周期管理(3) 自动播放下一条视频机制4.解决方案:整体架构设计短视频连续播放功能主要通过以下核心组件协同实现:ShortVideoPage:页面主体,负责视频列表管理、滑动切换控制和全局状态协调ShortVideoView:单个视频播放组件,独立管理自己的播放器实例和播放状态Swiper:垂直滑动组件,实现视频间的流畅切换体验核心实现机制详解(1) Swiper 滑动切换机制 Swiper(this.swiperController) { LazyForEach(this.lazyVideoListData, (item: ChannelItemBean, index: number) => { ShortVideoView({ // ... 参数传递 }) })}.vertical(true) // 垂直滑动模式,模拟抖音式浏览体验.index(this.curIndex) // 当前播放视频索引,用于定位当前播放项.onChange((index: number) => { this.onSwiperItemChange(index) // 滑动切换时的回调处理,包括数据更新和统计上报})(2) 视频播放器生命周期管理每个 ShortVideoView 组件采用独立的播放器生命周期管理策略: // ShortVideoView.ets@Link @Watch('createOrFinish') selectedVideoUrl: string; // 当前选中播放的视频URL// 基于选中状态精确控制播放器的创建和销毁createOrFinish(): void { if (this.videoUrl == this.selectedVideoUrl) { this.init(); // 初始化并开始播放当前视频 } else { this.getStop(); this.release(); // 停止并释放非当前播放的视频资源 }}3) 自动播放下一条视频机制通过事件发射器机制实现自动播放下一条视频: // 在 ShortVideoView.ets 中,播放完成时发送事件case 'completed': emitter.emit(EventConst.SHORT_VIDEO_SHOW_NEXT) // 发送播放完成事件通知 this.removeRecentVideo() // 移除已播放完成的视频进度记录 this.callbackComplete?.(); this.showLoading = false break;// 在 ShortVideoPage.ets 中监听事件并处理自动播放private onEmitter() { // 注册监听播放完成事件,实现续播下一条视频 emitter.on(EventConst.SHORT_VIDEO_SHOW_NEXT, () => { this.onAutoNext() })}private onAutoNext() { Logger.info(TAG, `onAutoNext`) this.swiperController.showNext() // 自动滑动到下一个视频项 this.atype = 'auto' // 标记为自动播放类型 this.ptype = 'order' // 标记为顺序播放模式}完整播放流程初始化播放:进入页面时,根据当前索引初始化第一个视频播放器,加载并播放视频内容滑动切换:用户上下滑动时,Swiper 的 onChange 回调更新当前索引,并触发相应业务逻辑播放器精细化管理:新显示的视频组件通过 @Watch 监听 selectedVideoUrl 变化调用 createOrFinish 方法决定是否初始化播放器实例隐藏的视频组件会自动停止播放并释放系统资源,避免资源浪费自动连续播放:当前视频播放完成后,通过事件机制通知页面自动切换到下一个视频,实现无缝续播体验关键优化技术点资源智能管理:通过 @Watch 和 createOrFinish 方法精确控制播放器的创建和释放,确保同时只有一个视频在播放,大大节省系统资源懒加载机制:使用 LazyForEach 实现视频列表的按需加载,提升页面初始化性能全局状态共享:通过 @Provide/@Consume 实现播放状态、控制器显示等全局状态的实时同步事件驱动架构:利用 emitter 实现组件间解耦通信,提高系统可维护性播放进度续播:通过 RECENT_VIDEOS 存储播放进度,支持用户中断后继续观看性能监控统计:完善的日志记录和播放数据统计,便于问题排查和用户体验优化这种精心设计的架构确保了在垂直滑动浏览短视频时能够提供流畅的连续播放体验,同时通过精细化的资源管理避免了系统性能问题。
-
router.pushUrl({ url:url }) this.getUIContext().getRouter().pushNamedRoute({ name:"qrCode" })//都不管用
上滑加载中
推荐直播
-
用码道,让你的AI作品三步上朋友圈2026/08/04 周二 19:00-20:00
林华鼎-华为云AI开发者运营负责人
从入门 · 到做AI应用 · 到企业级开发。不教编程,只教用AI · 零代码、有产出、能带走、可炫耀 · 每课人人动手实操
回顾中 -
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
基于华为云码道,构建你的定制化AI搭子2026/08/14 周五 09:00-11:30
明亮-华为云开发者发展与支持部部长
本期直播将向您全面介绍华为云码道产品,并基于码道手把手教你部署自己的定制化AI陪伴搭子。
回顾中
热门标签