惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs
博客园 - 叶小钗
P
Privacy International News Feed
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
IT之家
IT之家
博客园 - 聂微东
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
罗磊的独立博客
I
Intezer
S
Schneier on Security
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest
H
Heimdal Security Blog
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

人人都是产品经理

为什么你的产品找不到差异化?90%的失败都卡在第一步上(下) – 人人都是产品经理, 3年从30万到1300万用户、获2200万美元融资,这个AI教育产品用“抽卡”破解了获客难题 – 人人都是产品经理, 园区招商系统怎么做才能真正帮到去化?我加了这一个功能,推广链接转发400次阅读过万 – 人人都是产品经理, AI大事件:OpenAI发完网络安全模型又搞药物研发,小鹏汽车要抓”DeepSeek时刻” – 人人都是产品经理, 电商不是卖货,是一场更残酷的产品经理实战 – 人人都是产品经理, 没想到,活动营销又回来了! – 人人都是产品经理, 为何All-in海外KOC:一场关于AI时代窗口期的豪赌 – 人人都是产品经理, 重新理解企业的内部协作 – 人人都是产品经理, 苹果的 AI 战略到底是什么? – 人人都是产品经理, 医疗智能体·第2讲——合规护城河:等保、PIPL与HIPAA的架构实战 – 人人都是产品经理, 向量知识库五步法:从“答非所问”到“精准回复” – 人人都是产品经理, 鸿蒙PC三方库构建总指挥HPKBUILD(sha)库为例 – 人人都是产品经理, 何时该用LLM?AI产品经理的LLM设计指南 – 人人都是产品经理, 医疗信息领域的需求方、决策方、准入方以及关注点(二) – 人人都是产品经理, 即梦涨价:一场被误读的「傲慢」 – 人人都是产品经理, 面试AI PM必答题:Hermes和OpenClaw的区别,如何讲清楚业务价值 – 人人都是产品经理, AI的下一张船票:世界模型——AI产品经理必须理解的技术拐点 – 人人都是产品经理, 小红书做GEO,怎么让AI信你?记住这 3 个重要信息 – 人人都是产品经理, 5 家印度 AI 初创公司,看看印度 AI 再做什么 – 人人都是产品经理, AI项目跨团队协作:产品技术业务如何不打架 – 人人都是产品经理, Agentic Workflow(智能体工作流):让AI从”答案生成器”变成”数字员工” – 人人都是产品经理, lycium_plusplus 项目全景解读:OpenHarmony 三方库构建的“大管家” – 人人都是产品经理, 从爆单救火到前置履约:两套预采策略,把生鲜大促履约效率拉满 – 人人都是产品经理, 什么时候该补货?我用一轮数据做了一个决定 – 人人都是产品经理, 从“机械兜底”到“动态分流”:AI客服重复进线治理的4大底层逻辑 – 人人都是产品经理, 抖音拼效率,红书拼洞察 – 人人都是产品经理, 全民狂欢与退潮——为什么龙虾这波热潮冷却得如此之快? – 人人都是产品经理, Stripe押注!MPP重塑全球支付 – 人人都是产品经理, 小红书GEO:AI引用你的内容,不是因为你对,而是因为你看起来可信 – 人人都是产品经理, 前百度副总裁押注办公Agent,日韩付费爆发,Manus迎来强劲对手 – 人人都是产品经理, 企事业单位数字化的业务供需本质 – 人人都是产品经理, 医疗智能体·第1讲——医疗信息化重构:从“辅助软件”到“自主智能体”的范式转移 – 人人都是产品经理, 粉丝量就是空气!!! – 人人都是产品经理, 用户说“薯片碎了”,机器回“要买吗?”:意图识别的翻车与破局 – 人人都是产品经理, RAG召回准确率从75到90 我做对了这三件事 – 人人都是产品经理, AI大事件:Anthropic改收费、OpenAI发安全版、手术机器人纳入医保、阿里发布”秒悟” – 人人都是产品经理, Chrome 推出 Skills 新功能,Agent 重塑上网方式 – 人人都是产品经理, GitHub前创始人拿了a16z的1700万美元,做Agent时代的Git – 人人都是产品经理 拷贝或克隆其他 Flutter OH 项目到本地后无法运行 – 人人都是产品经理, 优惠券设计:优惠券创建 – 人人都是产品经理, 不用死磕文档!AI 助手 1 小时搞定飞书 CLI 安装 + 配置 + 知识库 – 人人都是产品经理, 用小龙虾做竞品分析报告:从2天到20分钟,我是怎么做到的 – 人人都是产品经理 用小龙虾做市场分析报告:搞懂这3个公式,市场规模不再靠猜 – 人人都是产品经理, 你早就在做 Harness 工程,只是不知道它叫这个名字 – 人人都是产品经理, Think Long就够?你可能想多了! – 人人都是产品经理, 货代SRM实战:供应商准入怎么做,才能让资源池不是通讯录而是可交付网络? – 人人都是产品经理, 如何做好用户调研?详解基本技巧 – 人人都是产品经理, 木鸟、途家、美团对打,平台春天行动开“卷” – 人人都是产品经理, 入职才发现公司不靠谱?小红书从业者求职避坑指南 – 人人都是产品经理, 美国 AI 三巨头联手封堵,中国 AI 突围之路在何方 – 人人都是产品经理, 小红书,放在需求对面的镜子 – 人人都是产品经理, AI 会带来大规模失业吗? – 人人都是产品经理, 从出单到补货前,我第一次犹豫:该不该放大? – 人人都是产品经理, Flutter 三方库鸿蒙化适配:5 种高效检查方式,快速判断是否需要适配 – 人人都是产品经理, 从做产品进阶拿结果:医美机构产品经理转岗科室运营经理 – 人人都是产品经理, 阿里HappyHorse,一场关于“Token经济”的阳谋 – 人人都是产品经理, To B AI:客户留存落地的观察与思考 – 人人都是产品经理, AI产品的“生命线”——数据采集、标注、清洗的产品化设计 – 人人都是产品经理, 谈谈AI Agent(二):当“孩子”能自己“体验世界”时,你该学什么? – 人人都是产品经理, UI/UX设计师的3层能力进阶,前两层让你活下来,第三层…才是真正的分水岭 – 人人都是产品经理, 2分钟 → 30秒,效率提升75%:B端产品经理如何用「规则枷锁」驯服AI幻觉? – 人人都是产品经理, 还没来得及学OpenClaw,来了个更猛的:Hermes Agent – 人人都是产品经理, AI日报:宇树机器人跑出10m/s刷新世界纪录 – 人人都是产品经理, 一文说透基金互金如何用情绪价值引导用户决策做转化 – 人人都是产品经理, 当浏览器开始替你”看”网页:AI 浏览器正在亲手拆掉它脚下的那张网 – 人人都是产品经理, 0代码,一天时间我Vibe Coding了个网站 – 人人都是产品经理, Hermes 和 OpenClaw 之争,Agent 的能力应该“装上去”还是“长出来”? – 人人都是产品经理 视频生成的“桌子”,字节Seedance 2掀完,阿里快乐马掀 – 人人都是产品经理, 从听不懂到完全信任:我的 Codex 深度产品体验 – 人人都是产品经理, 当虚拟偶像有了北京户口,与真人偶像还有什么区别? – 人人都是产品经理, 会说,远远比会做更重要 —— 对 SBTI 爆火现象的五层观察 – 人人都是产品经理, AI产品经理必看:当“搭环境”比“选模型”更重要,你的认知还在2024年吗? – 人人都是产品经理, 2026年AI产品商业化核心逻辑:从功能demo到规模化营收的3个必破卡点 – 人人都是产品经理, 京东围绕供应链,卷起裤腿下场的那些事儿 – 人人都是产品经理, SBTI一夜刷屏:它赢在了“太会说人话” – 人人都是产品经理, 折扣零售的真相:不是便宜,而是价值感! – 人人都是产品经理, 和甲方吵了一架,最后加钱做了——我学到的ToB产品经理生存法则 – 人人都是产品经理, 和几位小红书操盘手聊了8小时,干货全在这 – 人人都是产品经理, 智谱GLM-5.1登场,开源模型首超Opus4.6!!! – 人人都是产品经理 Anthropic收入凭什么反超OpenAI,终于有人把这事说清楚了 – 人人都是产品经理, 史上最有故事感的技术报告——Claude最强模型Mythos 7个极其精彩的细节 – 人人都是产品经理, 模型不是壁垒,Harness 也不是 – 人人都是产品经理, 抖音本地生活业务思考21 – 人人都是产品经理, Superpowers:145k Star的AI编码框架,到底是什么来头? Superpowers:145k Star的AI编码框架,到底是什么来头? – 人人都是产品经理, OpenAI 的路走错了,Anthropic Harness 解法启示:模型需要实践专科生 – 人人都是产品经理, 画原型图的前一步:设计站点地图 – 人人都是产品经理, 给 DeepSeek 的最后一封催更信 – 人人都是产品经理, 手把手教你用 Claude Code 搭建 AI 营销团队:5 个 Agent、12 项技能,独立完成研究、写作、设计全流程 – 人人都是产品经理, 你以为大模型在学语言?不,它在重新发明语言学 – 人人都是产品经理 所谓Skill,不过是AI时代的工业垃圾 – 人人都是产品经理, 聊一聊内容传播的几个方法 – 人人都是产品经理, 当平台开始吃掉生态:从 OpenClaw 被封杀,读懂 Anthropic 的这盘棋 – 人人都是产品经理, 你装了 10 个 AI 插件,Obsidian 还是一个文件夹 – 人人都是产品经理 关于AI智能体架构演进的系统性思考:从单体试水到多体协同的重构 – 人人都是产品经理, 当“人”变成Skill,我们又该何去何从? – 人人都是产品经理 Mythos 事件:前沿 AI 治理的意外实验 – 人人都是产品经理, 货代CRM:信用与风险管理怎么做,才能把坏账风险拦在放货之前? – 人人都是产品经理, 从HR收集自拍照到员工自助录入——我见证了园区人脸识别从”不可用”到”真好用”的全过程 – 人人都是产品经理 千问闯关AI混沌期:阿里画靶,吴嘉张弓,马云射箭? – 人人都是产品经理,
【案例+1】HarmonyOS官方模板优秀案例(第10期:教育行业 · 课堂应用)
华为开发者联盟 · 2025-10-27 · via 人人都是产品经理

💡 鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞 💡

★ 一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★

在之前第三期为大家介绍了“教育备考案例”(^_^点这里立即回顾

本期带来新上模板“课堂应用模板”的案例解析

👉 覆盖20+行业,点击查看往期案例汇总贴,持续更新,点击收藏!一键三连!常看常新!

【第10期】教育行业 · 课堂应用

概述

行业洞察

  • 行业诉求:
    • 精准分发:面对不同的受众教育类应用有不同的业务场景,产出适配内容、精准题库等,并以高效且契合场景的方式进行分发,最终实现优质教育资源的有效传递。
    • 高效流畅、操作敏捷是教育类应用不同场景重要诉求,在线学习、考试等场景出现卡顿会严重影响学习、考试的。
    • 需具备智能刷题与精准辅导能力:基于大数据和算法,依据答题情况判断用户知识掌握状况,动态调整出题难度,推送契合的学习内容。
    • 多端协同能力:实现手机、平板、PC 端数据实时互通,提供离线功能,便于用户利用碎片化时间用于刷题或知识点复习,并可在线后同步学习进度。
  • 行业常用三方SDK

SDK链接:腾讯浏览服务SDK高德定位SDK百度地图SDK 极光Push SDK火山引擎SDK同盾SDK友盟SDK微信支付SDK支付宝SDK听云SDK福昕 PDF SDK网易云信 IM SDK保利威视频直播webSDK支付宝分享SDK

说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”

案例概览下载模板

基于以上行业分析,本期将介绍鸿蒙生态市场教育类行业模板——课堂应用模板,为行业提供常用功能的开发案例,模板主要分首页、学习、我的三大模块。

  • Stage开发模型 + 声明式UI开发范式。
  • 分层架构设计 + 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
  • 本模板已集成华为账号、广告等服务,只需做少量配置和定制即可快速实现华为账号的登录、媒体播放等功能。

本模板主要页面及核心功能如下所示:

教育学习应用模板

  ├──首页                           

  │   ├──顶部栏-搜索  

  │   │   ├── 搜索                          

  │   │   └── 职级分类选择                      

  │   │         

  │   ├──课堂列表    

  │   │   ├── 动态布局                                             

  │   │   ├── 信息流                         

  │   │   └── 功能区                      

  │   └──课堂详情    

  │       ├── 图文                                             

  │       ├── 收藏、评论                         

  │       ├── 分享(微信、朋友圈、QQ、生成海报、复制链接等)

  │       └── 视频                      

  ├──学习                           

  │   ├──顶部栏                        

  │   │   └── 搜索                      

  │   ├──课堂列表    

  │   └──视频详情页         

  │       ├── 竖屏播放

  │       ├── 横屏播放

  │       ├── 暂停、播放、进度调节、倍速、选集、画中画

  │       └── 收藏、评论、分享                            

  │

  └──我的                           

      ├──登录  

      │   ├── 华为账号一键登录                          

      │   ├── 微信登录                                                   

      │   ├── 账密登录

      │   └── 用户隐私协议同意                       

      │         

      ├──个人主页         

      │   └── 头像、昵称、简介

      │                    

      ├──功能栏    

      │   ├── 我的课程                                        

      │   ├── 我的收藏                   

      │   ├── 我的下载                             

      │   └── 我的错题

      │

      └──常用服务    

          ├── 订单                                        

          ├── 消息        

          ├── 评论    

          ├── 观看记录           

          ├── 意见反馈                     

          └── 设置

               ├── 编辑个人信息             

               ├── 隐私设置           

               ├── 播放设置             

               ├── 清理缓存           

               ├── 检测版本

               ├── 关于我们

               └── 退出登录

应用架构设计

分层化模块设计

产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。

本实践支持直板机、折叠机,为单HAP包形式,包含路由根节点、底部导航栏等。

基础特性层:用于存放相对独立的功能UI和业务逻辑实现。

本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块。

每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。

公共能力层:存放公共能力,包括公共UI组件、数据管理、外部交互和工具库等共享功能。

本实践的公共能力层分为公共基础能力和可分可合组件,均打包为HAR包被上层业务组件引用。

公共基础能力包含日志、文件处理等工具类,公共类型定义,网络库,以及弹窗、加载等公共组件。

可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。

本模板详细工程结构可见工程结构章节。

业务组件设计

为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。

行业场景技术方案

视频播放控制层定制化

1)场景说明

用户可播放网络视频,视频播放支持加解锁、画中画、倍速、等功能。

2)技术方案

  • 基于AVPlayer实现视频播放能力。
  • 使用XComponent渲染视频画面。
  • ArkUI搭建控制层操作能力。

画中画

1)场景说明

用户可启动画中画功能同时操作其他界面不影响视频观看。

2)技术方案

  • 采用XComponent实现画中画能力。
  • 封装PipManager工具类封装画中画相关API。

模板代码

工程结构下载模板

详细代码结构如下所示:

Course

├──commons

│  ├──commonlib/src/main/ets                              // 基础模块

│  │    ├──components

│  │    │   ├──BottomEditBar.ets                          // 底部编辑组件

│  │    │   ├──CourseItemComponent.ets                    // 课程列表组件

│  │    │   ├──DeleteDialog.ets                           // 删除按钮组件

│  │    │   ├──DialogComponent.ets                        // 自适应弹框组件

│  │    │   ├──GlobalAttributeModifier.ets                // 自定义动态属性

│  │    │   ├──LeftSwipeComponent.ets                     // 编辑组件

│  │    │   ├──NoData.ets                                 // 空数据页面

│  │    │   └──TopBar.ets                                 // 标题栏

│  │    ├──constants

│  │    │   ├──CommonConstants.ets                        // 常量

│  │    │   └──CommonEnums.ets                            // 路由页面

│  │    ├──models

│  │    │   ├──CollectionModel.ets                        // 收藏

│  │    │   ├──DownloadModel.ets                          // 下载

│  │    │   ├──MyCourseModel.ets                          // 我的课程

│  │    │   ├──OrderInfo.ets                              // 订单数据模型

│  │    │   ├──TopicItemModel.ets                         // 题目数据模型

│  │    │   ├──UserInfo.ets                               // 用户信息

│  │    │   ├──WindowSize.ets                             // 窗口数据模型

│  │    │   └──WrongQuestionModel.ets                     // 错题数据模型

│  │    └──utils

│  │        ├──DialogUtil.ets                             // 弹框

│  │        ├──Logger.ets                                 // 日志

│  │        ├──NumberUtil.ets                             // 数据格式化

│  │        ├──PermissionUtil.ets                         // 权限

│  │        ├──PreferenceUtil.ets                         // 首选项

│  │        └──RouterModule.ets                           // 路由

│  │

│  │

│  └──http/src/main/ets                                   // 网络模块

│       ├──apis

│       │    ├──Apis.ets                                  // 请求接口

│       │    └──HttpRequest.ets                           // 请求对象

│       ├──mocks

│       │    ├──AxiosMock.ets                             // mock

│       │    ├──RequestMock.ets                           // 请求mock数据

│       │    └──Data

│       │         └──CourseData.ets                       // 数据源

│       ├──model

│       │   └──CourseModel.ets                            // 课程数据模型

│       ├──tools

│       │   └──ConvertTool.ets                            // 模型转换工具

│       └──types

│           └──Course.ets                                 // 课程数据协议

├──components

│  ├──aggregated_payment                                       // 支付组件

│  ├──aggregated_share                                     // 分享组件

│  ├──classification                                       // 分类组件

│  ├──feed_back                                            // 意见反馈组件

│  ├──login_info                                           // 登录组件

│  ├──open_ads                                             // 广告组件

│  ├──search                                               // 搜索组件

│  └──recorded_player                                      // 视频播放组件

├──features

│  ├──course/src/main/ets                                  // 学习课程模块

│  │    ├──component

│  │    │   └──CourseStudyComponent.ets                    // 课程数据源

│  │    ├──viewModels

│  │    │   └──CourseVM.ets                                // 课程数据源

│  │    └──views

│  │        └──MyLearnPage.ets                             // 学习页面

│  │

│  ├──home/src/main/ets                                    // 首页模块

│  │    ├──components

│  │    │   ├──AnswerQuestionsComponent.ets                // 答题view

│  │    │   ├──CommentPopup.ets                            // 评分弹框

│  │    │   ├──CourseCatalogComponents.ets                 // 课程章节弹框

│  │    │   ├──LiveBroadcastSign.ets                       // 直播标签view

│  │    │   ├──LiveNumberOverlay.ets                       // 数量view

│  │    │   ├──LiveStreamingItem.ets                       // 直播列表item

│  │    │   ├──AnswerSheetDialog.ets                       // 答题卡view

│  │    │   └──HomeTop.ets                                 // 首页职能类型选择页面

│  │    ├──models

│  │    │   ├──Practice.ets                                // 模块数据

│  │    │   ├──HomeMenuModel.ets                           // 课程数据

│  │    │   ├──SelectRoleModel.ets                         // 职称级别数据

│  │    │   └──Score.ets                                   // 打分数据

│  │    ├──viewModels

│  │    │   ├──CourseCatalogVM.ets                         // 章节数据刷新

│  │    │   └──HomeVM.ets                                  // 首页数据刷新

│  │    └──pages

│  │        ├──AnswerQuestionsExamPage.ets                 // 答题页面

│  │        ├──AnswerQuestionsPage.ets                     // 答题view

│  │        ├──AnswerSheetPage.ets                         // 答题卡页面

│  │        ├──ChapterDetail.ets                           // 章节详情页面

│  │        ├──CourseListPage.ets                          // 课程页页面

│  │        ├──ExamResultPage.ets                          // 答题结果页面

│  │        ├──HomePage.ets                                // 首页

│  │        ├──HomeSearch.ets                              // 搜索页面

│  │        ├──LiveStreamingListPage.ets                   // 直播页面

│  │        ├──MaterialPage.ets                            // 资料页面

│  │        ├──TestReportPage.ets                          // 结果页面

│  │        └──RecordedDetail.ets                          // 课程详情页面

│  └──mine/src/main/ets                                    // 个人模块

│       ├──viewModel

│       │   └──MyVM.ets                                    // 数据处理层

│       ├──models

│       │   ├──MessageModel.ets                            // 消息数据

│       │   ├──SystemMessageModel.ets                      // 系统消息数据

│       │   └──ServiceModel.ets                            // 模块服务数据

│       ├──component

│       │   ├──CourseHistoryComponent.ets                  // 观看记录item

│       │   └──TopicItemComponent.ets                      // 题目标题

│       └──views

│           ├──AboutAndVersion.ets                         // 关于页面

│           ├──AboutPage.ets                               // 设置页面

│           ├──Authentication.ets                          // 用户协议页面

│           ├──CourseUpdateMessagePage.ets                 // 课程动态消息页面

│           ├──DataSharingPage.ets                         // 三方信息页面

│           ├──FeedbackPage.ets                            // 意见反馈页面

│           ├──FeedbackRecordsPage.ets                     // 反馈记录

│           ├──MessageListCard.ets                         // 消息列表组件页面

│           ├──MessagePage.ets                             // 消息页面

│           ├──MinePage.ets                                // 个人页面

│           ├──MultiLineText.ets                           // 消息组件页面

│           ├──MyCollectionPage.ets                        // 收藏页面

│           ├──MyCourseComments.ets                        // 课程评论页面

│           ├──MyCoursePage.ets                            // 课程页面

│           ├──MyDownloadPage.ets                          // 下载页面

│           ├──MyOrderPage.ets                             // 订单页面

│           ├──MyWrongQuestionPage.ets                     // 错题页面

│           ├──OrderListPage.ets                           // 订单列表页面

│           ├──OrderDetailPage.ets                         // 订单列表页面

│           ├──PersonalInfoPage.ets                        // 个人信息页面

│           ├──PersonalInformationCollectionPage.ets       // 个人信息收集页面

│           ├──PrivacyAgreement.ets                        // 隐私协议页面

│           ├──PrivacySetPage.ets                          // 隐私设置页面

│           ├──PrivacyStatement.ets                        // 隐私声明页面

│           ├──SetUpPage.ets                               // 设置页面

│           ├──SystemMessage.ets                           // 系统消息页面

│           ├──UpdateVersionContentCard.ets                // 版本更新页面

│           ├──WatchHistoryPage.ets                        // 观看记录页面

│           └──UserAgreement.ets                           // 服务协议页面

└──product

└──entry/src/main/ets

├──viewmodels                                      // 首页数据

│   └──MainEntryVM.ets

├──entryability                                    // 入口

│   └──EntryAbility.ets

├──entrybackupability                              // 后台

│   └──EntryBackupAbility.ets

├──models

│   ├──RouterTable.ets                             // 路由表

│   └──Types.ets                                   // tab数据类型

└──pages

├──Index.ets                                    // 入口页面

├──Launch.ets                                   // 启动页面

├──LaunchAd.ets                                 // 广告页面

├──Login.ets                                    // 登录页面

├──MainEntry.ets                                // 主页面

├──PrivacyPolicy.ets                            // 隐私页面

└──PrivacyPolicyAlert.ets                       // 隐私弹框页面

关键代码解读

本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。

  • 播放器工具封装
@ObservedV2
export class AVPlayer {
callbackComplete: () => void = () => {
};
callbackTimeUpdate: (nol: number, total: number) => void = (nol: number) => {
};
callbackBitrateUpdate: (bitrateList: number[]) => void = (bitrateList: number[]) => {
};
callbackErrorUpdate: (error: string) => void = (error: string) => {
};
@Trace currentTime: number = 0;
@Trace totalDuration: number = 0;
@Trace isPlaying: boolean = false;
@Trace isLoading: boolean = true;
@Trace isLoadError: boolean = false;
// surfaceID用于播放画面显示,具体的值需要通过XComponent接口获取,相关文档链接见上面XComponent创建方法
surfaceID: string = ”;
// 画中画是否开启
isPipOpen: boolean = false;
// 复原
isPiPWindowRestore: boolean = false;
avPlayer: media.AVPlayer = {} as media.AVPlayer;
isCreate: boolean = false;
seekTime: number = 0;
// 碰到特殊场景进入其他页时,即便加载成功也不需要播放
isResetPause: boolean = false;
videoUrl: string = ”;async init(url: string, seekTime: number): Promise<void> {
await this.release();
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
await this.setStateChangeCallback();
await this.setSourceInfoCallback();
this.isCreate = true;
this.isResetPause = false;
this.videoUrl = url
this.avPlayer.url = url;
this.isLoading = true;
this.seekTime = seekTime;
}

getPlay() {
if (this.avPlayer) {
this.avPlayer.play();
}
}

getPause() {
if (this.avPlayer) {
this.avPlayer.pause();
}
}

getStop() {
if (this.avPlayer) {
this.avPlayer.stop();
}
}

changeVolume(volume: number) {
if (this.avPlayer) {
this.avPlayer.setVolume(volume)
}
}

changeSpeed(speed: number) {
if (this.avPlayer) {
this.avPlayer.setSpeed(speed)
}
}

changeSeek(seekTime: number, mode: SliderChangeMode) {
if (this.avPlayer) {
this.avPlayer.seek(seekTime, 2);
}
}

setSurfaceId(surfaceId: string): void {
if (this.avPlayer.surfaceId === surfaceId) {
return
    }
this.surfaceID = surfaceId
this.avPlayer.surfaceId = this.surfaceID;
}

// 切换播放
async changePlay(url: string, seekTime: number) {
if (this.avPlayer) {
setTimeout(() => {
this.isLoadError = false
      }, 1000)
this.isPlaying = false
      this.isLoading = true
      this.isResetPause = false;
await this.avPlayer.reset()
this.totalDuration = 0
this.seekTime = seekTime
this.videoUrl = url
this.avPlayer.url = url
}
}

setCompleteCallback(func: () => void) {
this.callbackComplete = func;
this.isPlaying = false
  }

setTimeUpdateCallback(func: (nol: number, total: number) => void) {
this.callbackTimeUpdate = func;
}

setBitrateUpdateCallback(func: (bitrateList: number[]) => void) {
this.callbackBitrateUpdate = func;
}

setErrorCallback(func: (error: string) => void) {
this.callbackErrorUpdate = func;
}

// 资源释放
async release() {
if (this.isCreate) {
this.isCreate = false;
this.isPlaying = false;
this.isLoading = false;
this.isLoadError = false;
this.isResetPause = false;
await this.avPlayer.release();
}
}

// 视频信息上报函数
async setSourceInfoCallback(): Promise<void> {
// 时间上报函数
this.avPlayer.on(‘timeUpdate’, (time: number) => {
this.currentTime = time
this.callbackTimeUpdate(time, this.totalDuration);
});
// 音量变化回调函数
this.avPlayer.on(‘volumeChange’, (vol: number) => {
});
// 视频播放结束触发回调
this.avPlayer.on(‘endOfStream’, () => {
});
// seek操作回调函数
this.avPlayer.on(‘seekDone’, (seekDoneTime: number) => {
});
// 视频总时长上报函数
this.avPlayer.on(‘durationUpdate’, (duration: number) => {
});
// 设置倍速播放回调函数
this.avPlayer.on(‘speedDone’, (speed: number) => {
});
// bitrate设置成功回调函数
this.avPlayer.on(‘bitrateDone’, (bitrate: number) => {
});
// 缓冲上报回调函数
this.avPlayer.on(‘bufferingUpdate’, (infoType: media.BufferingInfoType, value: number) => {
});
// 首帧上报回调函数
this.avPlayer.on(‘startRenderFrame’, () => {
});
// 视频宽高上报回调函数
this.avPlayer.on(‘videoSizeChange’, (width: number, height: number) => {
});
// 焦点上报回调函数
this.avPlayer.on(‘audioInterrupt’, (info: audio.InterruptEvent) => {
});
// HLS上报所有支持的比特率
this.avPlayer.on(‘availableBitrates’, (bitrates: number[]) => {
this.callbackBitrateUpdate(bitrates);
});
// 设置错误监听
this.avPlayer.on(‘error’, (error) => {
this.callbackErrorUpdate(‘Error ‘ + error.code + ‘ – ‘ + error.message);
});
}

// 状态机
async setStateChangeCallback(): Promise<void> {
this.avPlayer.on(‘stateChange’, async (state: string, reason: media.StateChangeReason) => {
if (this.avPlayer == null) {
return;
}
switch (state) {
// 闲置状态,成功调用reset接口后触发该状态机上报
case AVPlayerState.IDLE:
this.isLoadError = false
          break;
// 资源初始化,avplayer设置播放源后触发该状态上报
case AVPlayerState.INITIALIZED:
if (this.surfaceID) {
this.setSurfaceId(this.surfaceID); // 设置显示画面,当播放的资源为纯音频时无需设置
this.avPlayer.prepare(); // 进入准备状态
}
break;
// 已准备状态
case AVPlayerState.PREPARED:
this.avPlayer.audioInterruptMode = audio.InterruptMode.INDEPENDENT_MODE;
this.totalDuration = this.avPlayer.duration;
this.currentTime = this.totalDuration * this.seekTime / 100
this.avPlayer.seek(this.currentTime, 2);
if (this.isResetPause) {
this.isPlaying = false;
this.avPlayer.pause()
}else {
this.isPlaying = true;
this.avPlayer.play();
}
this.changeSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X)
this.isLoadError = false;
break;
// 正在播放状态
case AVPlayerState.PLAYING:
this.isLoading = false;
this.isPlaying = true;
break;
// 暂停状态
case AVPlayerState.PAUSED:
this.isLoading = false;
this.isPlaying = false;
break;
// 播放至结尾状态
case AVPlayerState.COMPLETED:
this.isPlaying = false
          this.callbackComplete();
break;
// 停止状态
case AVPlayerState.STOPPED:
break;
// 销毁状态,销毁与当前AVPlayer关联的播放引擎,无法再进行状态转换
case AVPlayerState.RELEASED:
this.isLoading = true;
break;
// 错误状态,当播放引擎发生不可逆的错误,则会转换至当前状态
case AVPlayerState.ERROR:
setTimeout(() => {
this.isPlaying = false;
this.isLoading = false;
this.isLoadError = true;
}, 1000)
break;
default:
break;
}
})
}
}

  • 画中画工具类封装
const CONTEXT_WIDTH: number = 1600;
const CONTEXT_HEIGHT: number = 900;export class CustomXComponentController extends XComponentController {
onSurfaceCreated(surfaceId: string): void {
if (PipManager.getInstance().player.surfaceID === surfaceId) {
return;
}
PipManager.getInstance().player.setSurfaceId(surfaceId);
}
}

// 画中画控制器
export class PipManager {
private static instance: PipManager = new PipManager();
private pipController?: PiPWindow.PiPController;
private mXComponentController: XComponentController;
player: AVPlayer;

constructor() {
this.player = new AVPlayer();
this.mXComponentController = new CustomXComponentController();
}

public static getInstance(): PipManager {
return PipManager.instance;
}

getXComponentController(): CustomXComponentController {
return this.mXComponentController;
}

init(ctx: Context, navigationId: string) {
// 当前设备如若不支持画中画则退出
if (!PiPWindow.isPiPEnabled()) {
return;
}
let config: PiPWindow.PiPConfiguration = {
context: ctx,
// XComponent组件绑定同一个
componentController: this.mXComponentController,
navigationId: navigationId,
// 画中画媒体类型枚举,当前使用的视频播放模版
templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY,
contentWidth: CONTEXT_WIDTH,
contentHeight: CONTEXT_HEIGHT,
};
PiPWindow.create(config).then((controller: PiPWindow.PiPController) => {
this.pipController = controller;
this.pipController?.setAutoStartEnabled(false);
this.pipController.on(‘stateChange’, (state: PiPWindow.PiPState, reason: string) => {
this.onStateChange(state, reason);
});
this.pipController.on(‘controlEvent’, (control: PiPWindow.ControlEventParam) => {
this.onActionEvent(control);
});
}).catch((err: BusinessError) => {
})
}

startPip() {
this.pipController?.startPiP().then(() => {
}).catch((err: BusinessError) => {
});
}

stopPip() {
this.pipController?.stopPiP().then(() => {
}).catch((err: BusinessError) => {
});
}

setAutoStart(autoStart: boolean): void {
this.pipController?.setAutoStartEnabled(autoStart);
}

updateContentSize(width: number, height: number) {
if (this.pipController) {
this.pipController.updateContentSize(width, height);
}
}

updatePiPControlStatus() {
let controlType: PiPWindow.PiPControlType = PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE;
let status: PiPWindow.PiPControlStatus = PiPWindow.PiPControlStatus.PLAY;
this.pipController?.updatePiPControlStatus(controlType, status);
}

unregisterPipStateChangeListener() {
this.pipController?.off(‘stateChange’);
this.pipController?.off(‘controlEvent’);
}

// 监听画中画生命周期
onStateChange(state: PiPWindow.PiPState, _reason: string) {
switch (state) {
// 表示画中画将要启动
case PiPWindow.PiPState.ABOUT_TO_START:
break;
// 表示画中画已经启动
case PiPWindow.PiPState.STARTED:
// 启动画中画
PipManager.getInstance().player.isPipOpen = true;
break;
// 表示画中画将要停止
case PiPWindow.PiPState.ABOUT_TO_STOP:
// 画中画关闭
PipManager.getInstance().player.isPipOpen = false;
break;
// 表示画中画已经停止
case PiPWindow.PiPState.STOPPED:
// 画中画关闭
PipManager.getInstance().player.isPipOpen = false;
// // 小窗口点击关闭画中画,视频暂停播放
PipManager.getInstance().player.isPiPWindowRestore = false;
break;
// 表示画中画将从小窗播放恢复到原始播放界面
case PiPWindow.PiPState.ABOUT_TO_RESTORE:
// 小窗口复原关闭画中画
PipManager.getInstance().player.isPipOpen = false;
// 从小窗口复原
PipManager.getInstance().player.isPiPWindowRestore = true;
break;
// 表示画中画生命周期执行过程出现了异常
case PiPWindow.PiPState.ERROR:
break;
default:
break;
}
}

// 监听画中画控制面板控件动作事件
onActionEvent(control: PiPWindow.ControlEventParam) {
switch (control.controlType) {
// 视频播放、停止
case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE:
if (control.status === PiPWindow.PiPControlStatus.PAUSE) {
// 停止视频
PipManager.getInstance().player.isPlaying = false;
} else if (control.status === PiPWindow.PiPControlStatus.PLAY) {
// 播放视频
PipManager.getInstance().player.isPlaying = true;
}
break;
// 切换到下一个视频
case PiPWindow.PiPControlType.VIDEO_NEXT:
break;
// 切换到上一个视频
case PiPWindow.PiPControlType.VIDEO_PREVIOUS:
break;
// 视频进度快进
case PiPWindow.PiPControlType.FAST_FORWARD:
break;
// 视频进度后退
case PiPWindow.PiPControlType.FAST_BACKWARD:
break;
default:
break;
}
}
}

模板集成

本模板提供了两种代码集成方式,供开发者自由使用。

整体集成

开发者可以选择直接基于模板工程开发自己的应用工程。

模板代码获取:

  • 通过IDE插件创建模板工程,开发指导
  • 通过生态市场下载源码,下载地址
  • 通过开源仓访问源码(待上架),仓库地址
    • 打开模板工程,根据README说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。

  • 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据。

将commons/http/src/main/ets/apis/Apis.ets文件中的mock接口替换为真实的服务器接口。

在commons/http/src/main/ets/types/Course.ets文件中将云侧开发者自定义的数据结构转换为端侧数据结构。

  • 根据自己的业务内容修改模板,进行定制化开发。

按需集成

若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。

组件代码获取:

  • 通过IDE插件创建模板工程,开发指导
  • 通过生态市场下载源码,下载地址
    • 下载组件源码,根据README中的说明,将组件包配置在自己的工程中。
    • 根据API参考和示例代码,将组件集成在自己的对应场景中。

以上是第10期“课堂应用”行业优秀案例的内容,更多行业敬请期待~

欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解XX行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~

同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新&活动不错过!

👉 HarmonyOS官方模板优秀案例系列持续更新, 点击查看往期案例汇总贴点击收藏方便查找!

👉【集成有礼】HarmonyOS官方模板集成创新活动,挥洒创意,赢精美大礼!点击参加

👉【HarmonyOS行业解决方案】为各行业鸿蒙应用提供全流程技术方案。点击查看