
【上新啦】HarmonyOS官方模板优秀案例(第2期:新闻行业 · 综合新闻)
💡 鸿蒙生态为开发者提供海量的HarmonyOS模板/组件,助力开发效率原地起飞 💡
★ 一键直达生态市场组件&模板市场, 快速应用DevEco Studio插件市场集成组件&模板★
HarmonyOS官方模板优秀案例第1期-便捷生活行业收到大家点赞
第2期-新闻行业小编加急上新啦!
本期案例内容更丰富,增加“预加载”、“AI朗读”等行业创新特性介绍
👉 覆盖20+行业,本帖以汇总形式持续更新中,点击收藏!一键三连!常看常新!
【第2期】新闻行业 · 综合新闻
一、概述
1.行业洞察
1)行业诉求:
- 新闻类应用中,核心关键是内容产出及内容分发效率,当前行业主要痛点是如何提升接收内容效率以及广告分发效率;
- 跨端场景使用是新闻类应用的重要场景诉求,尤其是PC、PAD、及车机端等全场景的使用及流转分发;
- 如何借助新技术实现精准内容分发,做到精准生产、精准传播、精准服务,是新闻类应用开发者在思考的问题;
- 新闻类应用需要为用户带来,自主可控、坚持贴近群众服务群众、易用、流畅、即时的应用使用体验。
2)行业常用三方SDK
分类 | 三方库名称 | 功能 | 支持情况 | SDK链接 |
媒体 | 阿里云视频播放器SDK | 音视频 | 已支持 | 极光PUSH SDK 个推 Bugly ShareSDK |
登录认证 | 中国移动一键登录SDK/易盾一键登录SDK/创蓝闪验/极光安全认证/阿里云号码认证SDK/中国电信一键登录SDK | 登录 | 已支持 | |
分享 | 友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK | 统计/推送/分享 | 已支持 | |
支付 | 支付宝支付/微信支付/银联支付 | 支付 | 已支持 | |
数据分析 | 友盟移动统计SD/神策数据SDK | 数据收集、处理、分析、运用 | 已支持 | |
性能监控 | 腾讯Bugly SDK/听云SDK/岳鹰全景监控SDK | 异常上报和运营统计 | 已支持 | |
地图 | 高德地图SDK | 地图 | 已支持 | |
推送 | 个推/华为推送/极光PUSH/阿里推送SDK | 消息推送 | 已支持 | |
媒体 | 阿里云视频播放器SDK | 音视频 | 已支持 |
说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”
2.优秀案例概览(下载模板)
综合新闻行业模板是基于以上行业分析实现的参考,为综合新闻类应用提供了常用功能的开发样例,涵盖新闻分类、列表、详情,精选视频,互动评论,内容分享等多个实用场景。
- Stage开发模型 + 声明式UI开发范式
- 分层架构设计 + 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。
- 集成华为账号、分享、扫码等一方服务和微信登录等三方SDK,开发者仅需做少量配置和定制即可快速体验相关能力。
- 集成预加载、推送服务、广告服务、AI朗读等创新特性,构建更丰富更全面的模板体验。
本模板主要页面及核心功能如下所示:
二、应用架构设计
1.分层模块化设计
- 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。
- 本实践暂时只支持直板机,为单HAP包形式,包含路由根节点、底部导航栏等。
- 基础特性层:用于存放相对独立的功能UI和业务逻辑实现。
- 本实践的基础特性层将应用功能拆分成6个相对独立的业务功能模块。
- 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。
- 公共能力层:存放公共能力,包括公共UI组件、数据管理、外部交互和工具库等共享功能。
- 本实践的公共能力层分为公共基础能力和行业组件,均打包为HAR包被基础特性层的业务模块引用。
- 公共基础能力包含账号管理、动态布局等工具,公共类型定义,网络库,以及弹窗、加载等公共组件。
- 行业组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。
本模板整体工程分包及介绍如下所示:
- 详细工程结构可见工程结构章节。
ComprehensiveNews ├──commons // 公共能力层-公共基础能力 │ ├──lib_account // 账号登录模块 │ ├──lib_common // 基础模块 │ ├──lib_flex_layout // 动态布局模块 │ ├──lib_native_components // 动态布局-原生模块 │ ├──lib_news_api // 服务端api模块 │ ├──lib_news_feed_details // 新闻详情模块 │ └──lib_widget // 通用UI模块 │ ├──components // 公共能力层-行业组件 │ ├──module_advertisement // 广告组件 │ ├──module_channeledit // 频道编辑组件 │ ├──module_feedback // 意见反馈组件 │ ├──module_feedcomment // 评论组件 │ ├──module_highlight // 高亮组件 │ ├──module_imagepreview // 图片预览组件 │ ├──module_newsfeed // 动态卡片组件 │ ├──module_post // 发帖组件 │ ├──module_setfontsize // 字体大小调节组件 │ ├──module_share // 分享组件 │ ├──module_swipeplayer // 视频组件 │ └──module_text_reader // 朗读组件 │ ├──features // 基础特性层 │ ├──business_home // 首页模块 │ ├──business_interaction // 互动模块 │ ├──business_mine // 我的模块 │ ├──business_profile // 个人主页模块 │ ├──business_setting // 设置模块 │ └──business_video // 视频模块 │ └──products // 产品定制层 └──phone // 手机设备的主入口模块 |
2.业务组件设计
为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。
例如开发者已搭建了一个自己的新闻工程,只想单独取用本模板中的广告功能,可根据本模板中module_advertisement模块的README文件中的快速入门步骤,将该模块单独集成在自己的工程中。
三、行业场景技术方案
1.账号管理
1)场景说明
支持华为账号一键登录及其他方式(账号密码登录、微信登录)。
用户登录后展示昵称和头像,点击用户信息栏可进入用户主页,查看并编辑个人信息和历史动态。
2)技术方案
- 华为账号一键登录
- 通过Account Kit实现华为账号一键登录,并获取用户手机号,关联应用已有用户。
- 微信登录
- 根据鸿蒙接入指南接入微信SDK,可通过跳转微信并获取微信用户授权进行登录。
- 头像修改
- 通过Scenario Fusion Kit提供的选择头像Button快速拉起头像选择页面,供用户完成华为账号头像或其他头像的选择与展示。
3)代码参考
- 部分核心代码参见账号管理实现章节。
2.内容分享
1)场景说明
- 用户可在首页-新闻详情,视频,互动等多个页面点击分享按钮,拉起分享面板。
- 用户可通过以下方式实现内容分享。
- 点击微信、朋友圈、QQ按钮拉起对应应用进行分享操作;
- 点击生成海报按钮将对应内容生成为携带二维码的海报,分享给上述应用或保存在本地;
- 点击复制链接将url存入剪切板;
- 点击系统分享拉起系统分享,支持华为分享,传入小艺智能空间等多种分享方式。
2)技术方案
- 微信分享
根据微信鸿蒙SDK接入指南接入微信SDK后,可通过分享功能开发手册调用指定API拉起微信并实现内容分享。
根据QQ鸿蒙SDK接入指南接入QQ SDK后,可通过调用分享接口拉起QQ并实现内容分享。
- 生成二维码海报并保存
通过系统组件QRCode将分享内容转换为字符串并写入,直接生成对应二维码。
通过弹窗授权保存媒体库资源的方式获取用户授权,并将海报存入相册,无需申请相册管理模块权限ohos.permission.WRITE_IMAGEVIDEO。
- 复制链接
通过Basic Services Kit中的pasteboard进行内容复制。
- 系统分享
通过Share Kit接入系统分享面板并发起分享。
3)代码参考
部分核心代码参见内容分享实现章节。
3.扫码识别
1)场景说明
用户可通过首页右上角的扫码按钮拉起扫码界面,通过摄像头或相册图片完成扫码操作。
应用识别出有效二维码信息可跳转并访问对应内容。
2)技术方案
3)代码参考
- 部分核心代码参见扫码识别实现章节。
4.视频播放
1)场景说明
支持竖屏、全屏、长按倍速视频播放,支持当前视频播放完毕后自动续播下一个。
支持用户上下滑动查看上一个或下一个视频。
2)技术方案
- 使用Swiper组件+LazyForEach懒加载实现上下滑动视频翻页的效果,并最大限度的降低应用内存占用,提升滑动帧率。
- 使用AVPlayer进行视频播放,实现播放事件的监听,及全屏、倍速等播放形式的设置。
- 使用gesture手势响应实现长按倍速等手势事件
3)代码参考
- 部分核心代码参见视频播放实现章节。
5.应用设置
1)场景说明
支持应用通用设置。
- 隐私设置:支持打开或关闭个性化推荐,关闭后首页顶部导航栏不展示推荐栏。
- 通知开关:支持打开或关闭推送通知,关闭后不接收云端推送消息。
- 播放与网络设置:支持修改非WLAN网络下的图片和视频播放设置,支持修改视频完播操作和自动播放设置。
- 清理缓存:支持一键清理应用内缓存。
- 夜间模式:支持手动开启或关闭应用内夜间模式。
- 字体大小:支持手动调节应用内字体大小。
- 检测版本:检测当前安装的应用版本是否为最新的应用市场在架版本。
2)技术方案
- 播放与网络设置
- 使用Network Kit管理网络连接,获取并监控设备的网络状态,进行全局存储。
- 在图片展示或视频播放页面获取网络状态变量,在流量状态下进行提示或省流处理。
- 清理缓存
- 通过Core File Kit查询应用缓存大小并展示。
- 使用基础文件操作接口获取缓存目录后遍历目录内的文件并删除。
- 夜间模式和字体大小
- 通过Ability Kit中环境变量的查询与设置实现应用深浅色模式和字体大小的切换。
- 检测版本
- 通过AppGallery Kit主动检查应用是否存在新版本,实现版本检测、显示更新提醒功能。
3)代码参考
- 部分核心代码参见应用设置实现章节。
四、行业场景创新特性
1.预加载
在应用启动前或初始化阶段,为避免出现首页内容加载慢、白屏等情况,可以通过接入预加载提升应用首开速度,提升用户体验。
本模板使用将首页推荐的新闻资源,在安装应用时通过预加载提前缓存到本地。用户首次访问应用时,直接从缓存中获取数据。减少了从服务器重新下载资源的时间,大大提升了应用首开速度。
同时集成了H5预加载组件FastWeb,对【我的-消息】中的私信对话H5界面进行预渲染、预编译,有效提升应用内H5的加载性能。
部分核心代码参见预加载实现章节。
2.广告
为实现流量变现,应用可以通过接入Ads Kit的流量变现服务实现横幅广告、开屏广告、贴片广告等多种形式的广告服务,向用户提供个性化的营销活动或商业广告。
本模板构建了开屏广告和原生广告,多样化呈现广告信息。
部分核心代码参见广告实现章节。
3.内容推送
应用可以通过接入Push Kit实现从云端到终端的消息推送通道,向应用实时推送消息,提升用户的感知度和活跃度。
本模板集成推送通知消息能力,向用户推送指定新闻内容,可在终端设备的通知中心、锁屏、横幅等展示,用户点击后拉起应用。
部分核心代码参见内容推送实现章节。
4.AI朗读
用户在接收新闻资讯时,除了常规的文本浏览,还可以通过AI朗读来获取信息。
本模板通过接入Speech Kit的朗读控件,提供了一键播放新闻朗读,并实现朗读内容滚动展示的功能,在用户不方便查看屏幕文字或想通过音频收听内容的时候获取新闻信息。
部分核心代码参见AI朗读实现章节。
五、模板代码
1.工程结构(下载模板)
详细代码结构如下所示:
ComprehensiveNews ├──commons │ ├──lib_account/src/main/ets // 账号登录模块 │ │ ├──components │ │ │ └──AgreePrivacyBox.ets // 隐私同意勾选 │ │ ├──pages │ │ │ ├──HuaweiLoginPage.ets // 华为账号登录页面 │ │ │ ├──OtherLoginPage.ets // 其他方式登录页面 │ │ │ └──ProtocolWebView.ets // 协议H5 │ │ └──utils │ │ ├──HuaweiAuthUtils.ets // 华为认证工具类 │ │ ├──LoginSheetUtils.ets // 统一登录半模态弹窗 │ │ └──WXApiUtils.ets // 微信登录事件处理类 │ │ │ ├──lib_common/src/main/ets // 基础模块 │ │ ├──constants // 通用常量 │ │ ├──datasource // 懒加载数据模型 │ │ ├──dialogs // 通用弹窗 │ │ ├──models // 状态观测模型 │ │ ├──push // 推送 │ │ └──utils // 通用方法 │ │ │ ├──lib_flex_layout/src/main/ets // 动态布局模块 │ │ ├──components │ │ │ ├──AddPlateComp.ets // 添加车牌组件 │ │ │ └──GuideListComp.ets // 指南列表组件 │ │ ├──sdk // 动态布局核心sdk │ │ └──views │ │ └──FlexLayout.ets // 动态布局列表页 │ │ │ ├──lib_native_components/src/main/ets // 动态布局-原生模块 │ │ ├──components │ │ │ ├──AdvertisementCard.ets // 广告卡片 │ │ │ ├──FeedDetailsCard.ets // 动态卡片 │ │ │ ├──HotListServiceSwitchCard.ets // 热榜切换组件 │ │ │ ├──HotNewsServiceCard.ets // 热榜新闻 │ │ │ ├──LeftTextRightImageCard.ets // 左文右图卡片 │ │ │ ├──TopTextBottomBigImageCard.ets // 上文下图大卡 │ │ │ ├──TopTextBottomImageCard.ets // 上文下图卡片 │ │ │ ├──TopTextBottomVideoCard.ets // 上文下视频卡片 │ │ │ └──VerticalBigImageCard.ets // 上下布局大图 │ │ └─utils │ │ ├──Modifier.ets // 样式modifier │ │ ├──NodeBuilderConfig.ets // node配置类 │ │ └──Utils.ets // 工具方法 │ │ │ ├──lib_news_api/src/main/ets // 服务端api模块 │ │ ├──constants // 常量文件 │ │ ├──database // 数据库 │ │ ├──observedmodels // 状态模型 │ │ ├──params // 请求响应参数 │ │ ├──services // 服务api │ │ └──utils // 工具utils │ │ │ ├──lib_news_feed_details/src/main/ets // 新闻详情模块 │ │ ├──components │ │ │ ├──ArticleDetailsFooter.ets // 文章底部区域 │ │ │ ├──NewsContent.ets // 新闻主体内容 │ │ │ └──RecommendArea.ets // 相关推荐 │ │ └──views │ │ └──ArticleFeedDetails.ets // 新闻详情页 │ │ │ └──lib_widget/src/main/ets // 通用UI模块 │ └──components │ ├──ButtonGroup.ets // 组合按钮 │ ├──CustomBadge.ets // 自定义信息标记组件 │ ├──EmptyBuilder.ets // 空白组件 │ └──NavHeaderBar.ets // 自定义标题栏 │ ├──components │ ├──module_advertisement // 广告组件 │ ├──module_channeledit // 频道编辑组件 │ ├──module_feedback // 意见反馈组件 │ ├──module_feedcomment // 评论组件 │ ├──module_highlight // 高亮组件 │ ├──module_imagepreview // 图片预览组件 │ ├──module_newsfeed // 动态卡片组件 │ ├──module_post // 发帖组件 │ ├──module_setfontsize // 字体大小调节组件 │ ├──module_share // 分享组件 │ ├──module_swipeplayer // 视频组件 │ └──module_text_reader // 朗读组件 │ ├──features │ ├──business_home/src/main/ets // 首页模块 │ │ ├──components │ │ │ └──NewsSearch.ets // 搜索页面 │ │ └──pages │ │ └──HomePage.ets // 首页页面 │ │ │ ├──business_interaction/src/main/ets // 互动模块 │ │ ├─components │ │ │ ├──InteractionFeedCard.ets // 动态卡片 │ │ │ ├──InterActionTabContent.ets // 动态列表 │ │ │ ├──NoWatcher.ets // 暂无关注 │ │ │ └──TopBar.ets // 顶部Tab │ │ └──pages │ │ ├──InteractionPage.ets // 互动主页面 │ │ └──PublishPostPage.ets // 发帖页面 │ │ │ ├──business_mine/src/main/ets // 我的模块 │ │ ├──components │ │ │ ├──BaseMarkLikePage.ets // 收藏点赞基础页面 │ │ │ ├──CancelDialogBuilder.ets // 取消收藏点赞弹窗 │ │ │ ├──CommentRoot.ets // 主评论 │ │ │ ├──CommentSub.ets // 从属评论 │ │ │ ├──FanItem.ets // 粉丝单元 │ │ │ ├──IMItem.ets // 私信单元 │ │ │ ├──MessageItem.ets // 消息单元 │ │ │ ├──SetReadIcon.ets // 标记已读 │ │ │ └──UniformNewsCard.ets // 统一新闻卡片 │ │ └──pages │ │ ├──CommentPage.ets // 评论页面 │ │ ├──HistoryPage.ets // 我的历史 │ │ ├──LikePage.ets // 我的点赞 │ │ ├──MarkPage.ets // 我的收藏 │ │ ├──MessageCommentReplyPage.ets // 评论与回复 │ │ ├──MessageFansPage.ets // 新增粉丝 │ │ ├──MessageIMChatPage.ets // 聊天页面 │ │ ├──MessageIMListPage.ets // 私信列表 │ │ ├──MessagePage.ets // 消息页面 │ │ ├──MessageSingleCommentList.ets // 全部回复页面 │ │ ├──MessageSystemPage.ets // 系统消息 │ │ └──MinePage.ets // 我的页面 │ │ │ ├──business_profile/src/main/ets // 个人主页模块 │ │ ├──components │ │ │ ├──AuthorItem.ets // 作者单元 │ │ │ ├──BaseFollowWatchPage.ets // 关注粉丝基础页面 │ │ │ ├──DialogLikeNum.ets // 获赞弹窗 │ │ │ ├──TabBar.ets // 顶部Tab │ │ │ ├──UniformNews.ets // 统一新闻卡片 │ │ │ ├──UserIntro.ets // 用户信息 │ │ │ └──WatchButton.ets // 关注按钮 │ │ └──pages │ │ ├──FollowerPage.ets // 粉丝页面 │ │ ├──PersonalHomePage.ets // 个人主页 │ │ └──WatchPage.ets // 关注页面 │ │ │ ├──business_setting/src/main/ets // 设置模块 │ │ ├──components │ │ │ ├──SettingCard.ets // 设置卡片 │ │ │ └──SettingSelectDialog.ets // 设置选项弹窗 │ │ └──pages │ │ ├──SettingAbout.ets // 关于页面 │ │ ├──SettingFont.ets // 字体大小设置页面 │ │ ├──SettingH5.ets // H5页面 │ │ ├──SettingNetwork.ets // 播放与网络设置页面 │ │ ├──SettingPage.ets // 设置页面 │ │ ├──SettingPersonal.ets // 编辑个人信息页面 │ │ └──SettingPrivacy.ets // 隐私设置页面 │ │ │ └──business_video/src/main/ets // 视频模块 │ ├──components │ │ ├──CommentView.ets // 评论视图 │ │ ├──Sidebar.ets // 侧边栏视图 │ │ ├──TabHeaderView.ets // 顶部Tab视图 │ │ └──VideoLayerView.ets // 视频外层操作层视图 │ ├──pages │ │ ├──CommentViewPage.ets // 评论页面 │ │ ├──VideoDetailPage.ets // 视频详情页 │ │ └──VideoPage.ets // 视频首页 │ └──views │ ├──FeaturedPage.ets // 精选页面 │ ├──FollowPage.ets // 关注页面 │ ├──RecommendPage.ets // 推荐页面 │ └──VideoSwiperPage.ets // 短视频轮播页面 │ └──products └──phone/src/main/ets // phone模块 ├──common │ ├──AppTheme.ets // 应用主题色 │ ├──Constants.ets // 业务常量 │ ├──FormUtils.ets // 卡片Utils │ └──Types.ets // 数据模型 ├──components │ └──CustomTabBar.ets // 应用底部Tab ├──pages │ ├──AgreeDialogPage.ets // 隐私同意弹窗 │ ├──Index.ets // 入口页面 │ ├──IndexPage.ets // 应用主页面 │ ├──PrivacyPage.ets // 查看隐私协议页面 │ ├──SafePage.ets // 隐私同意页面 │ ├──SplashPage.ets // 开屏广告页面 │ └──StartPage.ets // 应用启动页面 └──widget // 服务卡片 |
2.关键代码解读
本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。
若需获取全量代码,请查看模板集成章节。
1)账号管理实现
- 华为账号一键登录
// ComprehensiveNews/commons/lib_account/src/main/ets/viewmodels/LoginVM.ets @ObservedV2 exportclassLoginVMextendsBaseViewModel{ // 封装登录按钮controller controller: loginComponentManager.LoginWithHuaweiIDButtonController = new loginComponentManager.LoginWithHuaweiIDButtonController() .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED) .onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => { if (response) { Logger.info(TAG, 'onClickLoginWithHuaweiIDButton success, response: ' + JSON.stringify(response)); } if (error) { this.handleHuaweiLoginFail(error); return; } this.huaweiLogin(); }) // ... } |
// ComprehensiveNews/commons/lib_account/src/main/ets/pages/HuaweiLoginPage.ets @ComponentV2 struct HuaweiLoginPage { @Local vm: LoginVM = LoginVM.getInstance(false); build() { NavDestination() { // ... Column() { // 集成华为账号一键登录按钮 LoginWithHuaweiIDButton({ params: { style: loginComponentManager.Style.BUTTON_CUSTOM, extraStyle: { buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({ show: true }).backgroundColor($r('app.color.app_theme')) }, loginType: loginComponentManager.LoginType.QUICK_LOGIN, supportDarkMode: false, }, controller: this.vm.controller, }).id('login_with_huaweiId_button') } } } } |
- 微信登录
// ComprehensiveNews/commons/lib_account/src/main/ets/viewmodels/LoginVM.ets @ObservedV2 exportclassLoginVMextendsBaseViewModel{ // 封装微信登录请求 asyncjumpWX() { const req = new wxopensdk.SendAuthReq; /** * 填写实际的请求信息 */ req.isOption1 = false; req.nonAutomatic = true; req.scope = 'snsapi_userinfo'; req.state = 'none'; req.transaction = 'test123'; const isJumpSuccess = await wxApi.sendReq(getContext() as common.UIAbilityContext, req); Logger.info(TAG, 'jump wechat result: ' + isJumpSuccess); if (!isJumpSuccess) { promptAction.showToast({ message: '未安装微信' }); } } // ... } |
2)内容分享实现
- 微信分享
// ComprehensiveNews/components/module_share/src/main/ets/viewModel/WxShareViewModel.ets import * as wXOpenSdk from'@tencent/wechat_open_sdk'; // ... exportclassWxShareViewModel{ // 此处换成真实appid wXApi = wXOpenSdk.WXAPIFactory.createWXAPI('wxc92d9d6570127a32') privatestatic _instance: WxShareViewModel isWXApp: boolean = this.wXApi.isWXAppInstalled() isWXAPPCallback(callback: Function) { if (!this.isWXApp) { AlertDialog.show({ message: JSON.stringify('请先安装微信') }) } else { callback() } } /** * 分享网页 * @param text 文本内容 */ newsWebShare(qrCodeInfo: ShareOptions) { this.isWXAPPCallback(async () => { const webpageObject = new wXOpenSdk.WXWebpageObject() /* * 填写真实web地址 * */ webpageObject.webpageUrl = '' const mediaMessage = new wXOpenSdk.WXMediaMessage() mediaMessage.mediaObject = webpageObject mediaMessage.title = qrCodeInfo.title mediaMessage.description = `${qrCodeInfo.createTime}` const thumbData = await getContext(this).resourceManager.getMediaContent($r('app.media.startIcon')) const thumbPixel = image.createImageSource(thumbData.buffer).createPixelMapSync() const thumbBuffer = await image.createImagePacker().packToData(thumbPixel, { format: 'image/png', quality: 100 }) mediaMessage.thumbData = newUint8Array(thumbBuffer) const req = new wXOpenSdk.SendMessageToWXReq() req.scene = wXOpenSdk.SendMessageToWXReq.WXSceneSession req.message = mediaMessage this.wXApi.sendReq(getContext(this) as common.UIAbilityContext, req) }) } // ... } |
- QQ分享
// ComprehensiveNews/components/module_share/src/main/ets/viewModel/QqShareViewModel.ets import { IQQOpenApi, OpenApiConfig, QQOpenApiFactory, ShareResultType } from'@tencent/qq-open-sdk'; // ... /** * QQ分享 * 实际业务请填写实际appid */ exportclassQqShareViewModel{ static appId = 102803516 privatestatic _instance: QqShareViewModel privatestatic qqOpenApi: IQQOpenApi shareData: QqShareData = new QqShareData() public initQqShare(): IQQOpenApi { if (!QqShareViewModel.qqOpenApi) { let openApiOption: OpenApiConfig = { forceEnableWeb: false, autoHandleAuthResult: true, } QqShareViewModel.qqOpenApi = QQOpenApiFactory.createApi(QqShareViewModel.appId, openApiOption) } return QqShareViewModel.qqOpenApi } /* * 需要根据业务获取实际签名 * */ publicgenSign(appId: string | number, shareDataStr: string, timestamp: number, nonce: number) { let totalStr = `POSTconnect.qq.com/share?appid=${appId}&nonce=${nonce}&ts=${timestamp}&${shareDataStr}` return'业务签名' } publichandleShareInfo(qrCodeInfo:ShareOptions) { /* * 生成时间戳并计算签名 * */ let timeStamp = Date.parse(newDate().toString()) / 1000 let nonce = Math.floor(Math.random() * 100000000 + 100) let shareContent: QqShareContent = { msg_style: 0, title: qrCodeInfo.title, url: '', picture_url: qrCodeInfo.coverUrl asstring, } let shareDataSign = this.genSign(QqShareViewModel.appId, qrCodeInfo.title, timeStamp, nonce) this.shareData.timestamp = timeStamp this.shareData.shareJson = JSON.stringify(shareContent) this.shareData.nonce = nonce this.shareData.shareJsonSign = shareDataSign } publicasyncshare(qrCodeInfo:ShareOptions) { this.handleShareInfo(qrCodeInfo) let result = await QqShareViewModel.qqOpenApi.share(2, this.shareData); // ... } } |
- 系统分享
// ComprehensiveNews/components/module_share/src/main/ets/utils/SystemShare.ets exportclassPosterShare{ // ... publicasyncshare(qrCodeInfo:ShareOptions) { let shareData: systemShare.SharedData = new systemShare.SharedData({ utd: utd.UniformDataType.TEXT, content: 'www.vmall.com/index.html?cid=128688', title: qrCodeInfo.title, description: `${qrCodeInfo.createTime}`, }); let controller: systemShare.ShareController = new systemShare.ShareController(shareData); let context = getContext(this) as common.UIAbilityContext; controller.show(context, { selectionMode: systemShare.SelectionMode.SINGLE, previewMode: systemShare.SharePreviewMode.DETAIL, }).then(() => { console.info('ShareController show success.'); }).catch((error: BusinessError) => { console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`); }); controller.on('dismiss',() => { ShareEventDispatcher.dispatchToClose() }) } } |
3)扫码识别实现
- 二维码信息识别
// ComprehensiveNews/commons/lib_common/src/main/ets/utils/NewsScan.ets exportclassNewsScan{ static TAG: string = 'NewsScan' publicscan() { // 定义扫码参数options let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true, }; try { // 可调用getContext接口获取当前页面关联的UIAbilityContext scanBarcode.startScanForResult(getContext(this), options).then((result: scanBarcode.ScanResult) => { // 解析码值结果跳转应用服务页 let resultParams: Record<string, string | NewsEnum> = JSON.parse(result.originalValue) if(!resultParams?.articleId) { promptAction.showToast({ message: '扫码错误,仅支持模板内文章海报生成的二维码' }) return } let routerParams = { 'id': resultParams?.articleId, } as Record<string, string> RouterToNews.routerToNewsById(routerParams.id) }).catch((error: BusinessError) => { promptAction.showToast({ message: '扫码错误' }) Logger.error(NewsScan.TAG, '[Scan CPSample]', `Failed to get ScanResult by promise with options. Code:${error.code}, message: ${error.message}`); }); } catch (error) { promptAction.showToast({ message: '扫码错误' }) Logger.error(NewsScan.TAG, '[Scan CPSample]', `Failed to start the scanning service. Code:${error.code}, message: ${error.message}`); } } } |
4)视频播放实现
- 视频跟手上下滑动效果实现
// ComprehensiveNews/components/module_swipeplayer/src/main/ets/views/VideoSwipePlayer.ets @ComponentV2({ freezeWhenInactive: true }) export struct VideoSwipePlayer { // ... build() { Stack({ alignContent: Alignment.TopStart }) { Swiper(this.swiperController) { LazyForEach(this.datasource, (item: VideoPlayerData, index: number) => { VideoPlayerView({ videoData: item, index: index, autoPlay: this.options?.autoPlay, currentIndex: this.currentIndex, totalCount: this.options?.totalCount, swipePlayerController: this.swipePlayerController, videoItemPlayerSession: this.videoItemPlayerSession, videoNetwork: this.videoNetwork, videoNetworkSetting: this.videoNetworkSetting, videoLayerBuilder: this.videoLayerBuilder, fullBtnBuilder: this.fullBtnBuilder, pathStack: this.pathStack, }) }, (item: VideoPlayerData, index: number) => item.getVideoId() + index); } .cachedCount(this.options?.cachedCount ?? 2) .width('100%') .height('100%') .vertical(true) .loop(false) .curve(Curve.Ease) .duration(300) .indicator(false) .backgroundColor(Color.Black) .effectMode(this.currentIndex === 0 ? EdgeEffect.None : EdgeEffect.Spring) .onChange((index: number) => { this.currentIndex = index; if (this.options?.swiperCallback?.onChange) { this.options.swiperCallback?.onChange(index); } }) .onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => { this.currentIndex = targetIndex; if (this.options?.swiperCallback?.onAnimationStart) { this.options.swiperCallback?.onAnimationStart(index, targetIndex, extraInfo); } }) } .width('100%') .height('100%') .id('player') } } |
5)应用设置实现
- 清理缓存
// ComprehensiveNews/commons/lib_common/src/main/ets/utils/FileUtils.ets exportclassFileUtils{ /** * 读取缓存大小,单位Byte */ staticgetCache() { return storageStatistics.getCurrentBundleStats().then((bundleStats: storageStatistics.BundleStats) => { Logger.info(TAG, 'getCurrentBundleStats successfully:' + JSON.stringify(bundleStats)); Logger.info(TAG, 'appSize :' + bundleStats.appSize); Logger.info(TAG, 'cacheSize :' + bundleStats.cacheSize); Logger.info(TAG, 'dataSize :' + bundleStats.dataSize); return bundleStats.cacheSize; }); } /** * 清除缓存 * @param cacheDir */ staticclearCache() { const cacheDir = getContext().cacheDir; Logger.info(TAG, 'clear cache dir:' + cacheDir); fs.listFile(cacheDir).then((filenames) => { for (let i = 0; i < filenames.length; i++) { let dirPath = cacheDir + '/' + filenames[i]; Logger.info(TAG, 'dir path:' + dirPath); let isDirectory: boolean = false; try { isDirectory = fs.statSync(dirPath).isDirectory(); } catch (e) { Logger.error(TAG, 'statSync error:' + JSON.stringify(e)); } if (isDirectory) { fs.rmdirSync(dirPath); } else { fs.unlink(dirPath).then(() => { Logger.info(TAG, 'remove file succeed'); }).catch((err: Error) => { Logger.error(TAG, 'remove file failed with error message: ' + err.message); }); } } }) } // ... } |
- 检测版本
exportclassAppGalleryUtils{ /** * 是否有更新版本 * @param context * @returns */ publicstatic checkAppUpdate(context: common.UIAbilityContext): Promise<boolean> { try { return updateManager.checkAppUpdate(context) .then((checkResult: updateManager.CheckUpdateResult) => { Logger.info(TAG, 'Succeeded in checking Result updateAvailable:' + checkResult.updateAvailable); return checkResult.updateAvailable === updateManager.UpdateAvailableCode.LATER_VERSION_EXIST; }).catch((error: BusinessError) => { Logger.error(TAG, `checkAppUpdate onError.code is ${error.code}, message is ${error.message}`); returnfalse; }); } catch (error) { Logger.error(TAG, `checkAppUpdate onError.code is ${error.code}, message is ${error.message}`); returnPromise.resolve(false); } } /** * 显示升级弹窗 * @param context */ publicstaticshowUpdateDialog(context: common.UIAbilityContext) { try { updateManager.showUpdateDialog(context) .then((resultCode: updateManager.ShowUpdateResultCode) => { Logger.info(TAG, 'Succeeded in showing UpdateDialog resultCode:' + resultCode); }) .catch((error: BusinessError) => { Logger.error(TAG, `showUpdateDialog onError.code is ${error.code}, message is ${error.message}`); }); } catch (error) { Logger.error(TAG, `showUpdateDialog onError.code is ${error.code}, message is ${error.message}`); } } } |
6)预加载实现
- 推荐新闻列表云函数
// ComprehensiveNews/preload/handler.js let myHandler = asyncfunction (event, context, callback, logger) { logger.info(`Input event: ${JSON.stringify(event)}`); let hmSystem = { status: 1, msg: "success", }; logger.info(`hmSystem: ${hmSystem}`); let recommendList = [ // ... ]; logger.info(`tableTitleArr: ${recommendList}`); let result = { hmSystem, recommendList }; let res = new context.HTTPResponse( result, { "faas-content-type": "json", }, "application/json", "200" ); callback(res); }; module.exports.myHandler = myHandler; |
- 端侧集成安装预加载
// ComprehensiveNews/products/phone/src/main/ets/phoneability/PhoneAbility.ets exportdefaultclassPhoneAbilityextendsUIAbility{ onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { // ... this.functionPreload(); } privatefunctionPreload() { // 获取安装预加载数据 try { cloudResPrefetch.getPrefetchResult(cloudResPrefetch.PrefetchMode.INSTALL_PREFETCH) .then((data: cloudResPrefetch.PrefetchResult) => { let result: RequestListData[] = (data.result as CloudRecommendListResult<RequestListData[]>).recommendList as RequestListData[] AppStorage.setOrCreate('recommendList', result) Logger.info('preloadTag', `Succeeded in getting install prefetch data, result: ${JSON.stringify(data.result)}`); }).catch(async (err: BusinessError) => { Logger.error('preloadTag', `Failed to get install prefetch data, code: ${err.code}, message: ${err.message}`); // 使用原有方式获取应用数据 let recommendList = await HomeServiceApi.queryHomeRecommendList('recommend'); AppStorage.setOrCreate('recommendList', recommendList) }) } catch (err) { Logger.error('preloadTag', `Failed to get install prefetch data, code: ${err.code}, message: ${err.message}`); } } // ... } |
7)广告实现
- 请求广告
// ComprehensiveNews/components/module_advertisement/src/main/ets/components/Advertisement.ets @ComponentV2 export struct AdvertisementComponent { // 请求广告 privateasync loadAd(adId: string): Promise<void> { // 1为全屏广告,8为横幅广告 if (this.adType === 1) { // 广告请求参数 this.adRequestParams = { // 广告位ID adId: adId, // 开屏广告类型 adType: this.adType, // 请求的广告数量 adCount: 1, // 开放匿名设备标识符 oaid: this.oaId, }; this.adDisplayOptions = { mute: true, }; this.adOptions = { allowMobileTraffic: 0, adContentClassification: 'A', tagForUnderAgeOfPromise: -1, tagForChildProtection: -1, }; // 广告请求回调监听 const adLoadListener: advertising.AdLoadListener = { // 广告请求失败回调 onAdLoadFailure: (errorCode: number, errorMsg: string) => { hilog.error(0x0000, 'testTag', `Failed to load ad. Code is ${errorCode}, message is ${errorMsg}`); }, // 广告请求成功回调 onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => { clearTimeout(this.timeOutIndex); if (this.isTimeOut) { return; } hilog.info(0x0000, 'testTag', 'Succeeded in loading ad'); this.ad = ads[0]; }, }; // 创建AdLoader广告对象 const adLoader: advertising.AdLoader = new advertising.AdLoader(this.context); this.timeOutHandler(); // 调用广告请求接口 adLoader.loadAd(this.adRequestParams, this.adOptions, adLoadListener); } else { this.adRequestParams = { adId: adId, adType: this.adType, // 宽和高当前只支持360*57和360*144两种尺寸 adWidth: 360, adHeight: 57, oaid: this.oaId, }; this.adDisplayOptions = { refreshTime: 30000, }; this.adOptions = { allowMobileTraffic: 0, tagForChildProtection: -1, tagForUnderAgeOfPromise: -1, adContentClassification: 'A', }; } } } |
8)内容推送实现
- 获取云端推送
// ComprehensiveNews/commons/lib_common/src/main/ets/push/PushUtils.ets exportclassPushUtils{ /** * 获取push推送token **/ publicstaticasync getTokenSyn(): Promise<string> { try { const pushToken = await pushService.getToken(); return pushToken; } catch (err) { let e: BusinessError = err as BusinessError; Logger.error('get token', 'Get push token catch error: %{public}d %{public}s'); return''; } } /** * 本地使用push rest api mock云端推送 **/ publicstaticasyncpushMessage(articleList: ESObject) { /* * 这里的Authorization需要开发者自行获取,详情见readme * */ axios.post('https://push-api.cloud.huawei.com/v3/388421841222475582/messages:send', PushUtils.pushMessageParams(articleList), { headers: { 'Content-Type': 'application/json', 'push-type': 0, 'Authorization': 'Bearer ', }, }) .then((resp: AxiosResponse) => { Logger.info('push success', JSON.stringify(resp)) }) .catch((err: AxiosError) => { Logger.info('push err' + err) }) } } |
9)AI朗读实现
- 朗读控件
// ComprehensiveNews/components/module_text_reader/src/main/ets/views/ReadNewsComponent.ets // 当前受部分API接入限制,仅支持状态管理V1版本 @Component struct ReadIcon { /** * 包名 */ @Prop bundleName: string = '' /** * 播放状态 */ @State readState: ReadStateCode = ReadStateCode.WAITING; /** * 用于显示当前页的按钮状态 */ @State isInit: boolean = false; /** * 播放加载数据,传入的格式id,title的text,author的text,date的text,bodyInfo内容,封面的PixelMap */ @State readInfo: TextReader.ReadInfo | undefined = undefined /** * 用于显示播放的id */ @Prop readInfoListString: string @Prop localCoverImage:PixelMap @State readInfoList: TextReader.ReadInfo[] | undefined = undefined /** * 用于显示播放的id */ @Prop currentId: string /** * 初始化 */ asyncinit() { const readerParam: TextReader.ReaderParam = { isVoiceBrandVisible: true, businessBrandInfo: { panelName: this.bundleName, panelIcon: $r('app.media.startIcon'), }, } try { let context: Context | undefined = this.getUIContext().getHostContext() if (context) { await TextReader.init(context, readerParam); this.isInit = true; } } catch (err) { console.error(`TextReader failed to init. Code: ${err.code}, message: ${err.message}`); } } asyncaboutToAppear() { this.readInfoList = JSON.parse(this.readInfoListString) as TextReader.ReadInfo[] this.readInfoList.forEach((item) => { if (item.id === this.currentId) { this.readInfo = item } }) this.init(); } onStateChanged = (state: TextReader.ReadState) => { if (this.readInfo?.id === state.id) { this.readState = state.state; } else { this.readState = ReadStateCode.WAITING; } } // 设置操作监听 setActionListener() { TextReader.on('stateChange', (state: TextReader.ReadState) => { this.onStateChanged(state) }); TextReader.on('requestMore', () =>this.onStateChanged); TextReader.on('eventPanel', (pe: TextReader.PanelEvent) => { // 监听点击上一条或者下一条音频 if (pe.click === 'BPC_03' || pe.click === 'BPC_04') { let event: emitter.InnerEvent = { eventId: 1, priority: emitter.EventPriority.IMMEDIATE, }; let eventData: emitter.EventData = { data: { state: pe.id, }, }; // Send an event with eventId 1 and the event content is eventData emitter.emit(event, eventData); } }); } build() { Column() { TextReaderIcon({ readState: this.readState }) .margin({ right: 20 }) .width(32) .height(32) .onClick(async () => { try { this.setActionListener(); if (this.readInfoList !== undefined) { await TextReader.start(this.readInfoList, this.readInfo?.id); } } catch (err) { console.error(`TextReader failed to start. Code: ${err.code}, message: ${err.message}`); } }) } .justifyContent(FlexAlign.Center) .height('100%') .width('70%') .alignItems(HorizontalAlign.End) } } |
3.模板集成
本模板提供了两种代码集成方式,供开发者自由选用。
1)整体集成(下载模板)
开发者可以选择直接基于模板工程开发自己的应用工程。
- 模板代码获取:
- 打开模板工程,根据README说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。
- 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据。
- ComprehensiveNews/commons/lib_news_api/src/main/ets/services文件中的接口当前为本地mock数据,开发者可根据业务需要替换为真实的服务器接口,并进行云侧数据结构与端侧数据结构的对接转换。
根据自己的业务内容修改模板,进行定制化开发。
2)按需集成
若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。
- 组件代码获取:
- 下载组件源码,根据README中的说明,将组件包配置在自己的工程中。
- 根据API参考和示例代码,将组件集成在自己的对应场景中。
以上是第二期“新闻行业-综合新闻”行业优秀案例的内容,更多行业敬请期待~
欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解XX行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~
同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新&活动不错过!
👉本系列持续更新,欢迎点击帖子末尾左下角“🌟”收藏本帖!
期数 | 帖子 | 链接 |
第1期 | HarmonyOS官方模板优秀案例 | 便捷生活行业 · 购物中心 | 点击查看 |
第2期 | HarmonyOS官方模板优秀案例 | 新闻行业 · 综合新闻 | 点击查看 |
第3期 | 小编加急整理中,敬请期待 |
👉 HarmonyOS组件模板相关推荐
