HarmonyOS NEXT 新闻资讯应用 · 服务模块 features/service 实现指南
开发环境:DevEco Studio 6.1.0 Release
SDK版本:HarmonyOS SDK 6.1.0(23) / API 23
开发语言:ArkTS
状态管理:V2(@ComponentV2系列装饰器)
前置阅读:common模块指南 · 整体架构指南
本篇详细讲解服务模块features/service的实现。这是应用中结构最精简的 feature 模块——仅有 1 个组件ServiceHome、1 个源文件、74 行源码。虽然代码量少,但完整展示了shadow 卡片阴影、Tabs + ForEach 动态生成、模板字符串动态描述等实用技巧。
效果
一、模块定位与架构角色
服务模块在三层架构中处于基础特性层(features),编译为 HAR 包:
产品定制层 product/phone (HAP) ↓ 依赖 基础特性层 features/news | features/video | features/live | features/personal | features/service ← 本模块 (HAR) ↓ 依赖 公共能力层 common (HAR)模块职责:提供各类服务入口(党媒云、社区融、法律服务、健康服务),仅包含 1 个组件:
| 组件 | 文件 | 行数 | 类型 | 功能 |
|---|---|---|---|---|
ServiceHome | components/ServiceHome.ets | 74 | @ComponentV2 | 服务主页,4 分类 Tab + shadow 卡片列表 |
特点:与 live 模块类似,服务模块没有 NavDestination 子页面,不需要@Consumer('pageStack')。但服务模块比 live 模块更精简——没有 @Builder 列表模板,仅一个ServiceCard@Builder。
二、模块配置详解
2.1 oh-package.json5
{ "name": "service", "version": "1.0.0", "description": "Please describe the basic information.", "main": "Index.ets", "author": "", "license": "Apache-2.0", "dependencies": { "common": "file:../../common" } }2.2 Index.ets 入口文件
export{ServiceHome}from'./src/main/ets/components/ServiceHome';仅 2 行,导出唯一组件。外部通过import { ServiceHome } from 'service'使用。
三、完整文件结构树
features/service/ ├── oh-package.json5 (12行) 模块配置 ├── Index.ets (2行) 统一导出 ├── src/main/ │ ├── ets/ │ │ └── components/ │ │ └── ServiceHome.ets (74行) 服务主页,Tabs+shadow卡片 │ └── resources/ (图片等静态资源)与 live 模块结构一致,没有pages/目录。
四、ServiceHome 服务主页
4.1 完整源码
import{StyleConstants}from'common';/** * 服务主页组件 * 使用 @ComponentV2 实现 * 展示党媒云服务内容 */@ComponentV2exportstruct ServiceHome{@LocalcurrentIndex:number=0;privateserviceItems:string[]=['党媒云','社区融','法律服务','健康服务'];@BuilderServiceCard(title:string,description:string){Column(){Text(title).fontSize(16).fontWeight(FontWeight.Bold).fontColor(StyleConstants.TEXT_PRIMARY)Text(description).fontSize(13).fontColor(StyleConstants.TEXT_SECONDARY).margin({top:8}).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})}.width('100%').padding(16).backgroundColor(Color.White).borderRadius(8).shadow({radius:2,color:'#0D000000',offsetY:1})}build(){Column(){// 顶部标题Text('服务').fontSize(20).fontWeight(FontWeight.Bold).fontColor(StyleConstants.TEXT_PRIMARY).margin({left:16,top:12,bottom:8}).width('100%')// 分类TabTabs({barPosition:BarPosition.Start,index:this.currentIndex}){ForEach(this.serviceItems,(item:string,index?:number)=>{TabContent(){Scroll(){Column({space:12}){this.ServiceCard(`${item}平台`,`提供优质的${item}内容和服务,涵盖最新资讯、深度报道、专家解读等。`)this.ServiceCard(`${item}矩阵`,`整合多渠道资源,构建全方位的${item}传播体系。`)this.ServiceCard(`${item}互动`,`搭建用户与${item}之间的桥梁,提供便捷的互动交流服务。`)}.padding({left:16,right:16,top:12,bottom:12})}}.tabBar(item)},(item:string)=>item)}.onChange((index:number)=>{this.currentIndex=index;}).layoutWeight(1)}.width('100%').height('100%').backgroundColor(StyleConstants.BG_COLOR)}}4.2 分段深度讲解
段落1 — 状态变量(第10-11行)
@LocalcurrentIndex:number=0;privateserviceItems:string[]=['党媒云','社区融','法律服务','健康服务'];| 变量 | 装饰器 | 说明 |
|---|---|---|
currentIndex | @Local | 当前选中 Tab 索引 |
serviceItems | private | 4 个服务分类名称 |
注意:没有@Consumer('pageStack'),因为服务模块当前没有子页面跳转需求。
段落2 — @Builder ServiceCard 卡片模板(第13-33行)
@BuilderServiceCard(title:string,description:string){Column(){Text(title)// 卡片标题.fontSize(16).fontWeight(FontWeight.Bold).fontColor(StyleConstants.TEXT_PRIMARY)Text(description)// 卡片描述.fontSize(13).fontColor(StyleConstants.TEXT_SECONDARY).margin({top:8}).maxLines(2)// 最多2行.textOverflow({overflow:TextOverflow.Ellipsis})// 超长截断}.width('100%').padding(16).backgroundColor(Color.White).borderRadius(8).shadow({radius:2,color:'#0D000000',offsetY:1})// 阴影}卡片样式:
- 白色背景 + 圆角 8 + 阴影 → 经典的 Material Design 风格卡片
.padding(16)四边统一 16vp 内边距.maxLines(2)+TextOverflow.Ellipsis:描述文字最多显示 2 行,超出部分以...截断
shadow 属性详解(下一节深入讲解):
.shadow({radius:2,color:'#0D000000',offsetY:1})| 参数 | 值 | 说明 |
|---|---|---|
radius | 2 | 阴影模糊半径,值越大阴影越模糊 |
color | '#0D000000' | 阴影颜色,#0D= 约 5% 透明度黑色 |
offsetY | 1 | 阴影垂直偏移,向下 1vp(模拟光源从上方照射) |
段落3 — build() 主布局(第35-73行)
布局层级:
Column (根容器,BG_COLOR 背景) ├── Text('服务') ← 20号加粗标题 └── Tabs (barPosition:Start) ← 4个分类Tab └── ForEach(serviceItems) → TabContent └── Scroll ← 可滚动 └── Column (space:12) ← 3张卡片 ├── ServiceCard('${item}平台', ...) ├── ServiceCard('${item}矩阵', ...) └── ServiceCard('${item}互动', ...)Tabs + ForEach 动态生成:
Tabs({barPosition:BarPosition.Start,index:this.currentIndex}){ForEach(this.serviceItems,(item:string,index?:number)=>{TabContent(){...}.tabBar(item)// 用分类名作为Tab标签},(item:string)=>item)}4 个分类各生成一个TabContent,.tabBar(item)直接使用分类名称(“党媒云”/"社区融"等)作为标签文字。
模板字符串动态生成描述:
this.ServiceCard(`${item}平台`,`提供优质的${item}内容和服务,涵盖最新资讯、深度报道、专家解读等。`)this.ServiceCard(`${item}矩阵`,`整合多渠道资源,构建全方位的${item}传播体系。`)this.ServiceCard(`${item}互动`,`搭建用户与${item}之间的桥梁,提供便捷的互动交流服务。`)当item = '党媒云'时,生成:
- 卡片1:标题 “党媒云平台”,描述 “提供优质的党媒云内容和服务…”
- 卡片2:标题 “党媒云矩阵”,描述 “整合多渠道资源,构建全方位的党媒云传播体系。”
- 卡片3:标题 “党媒云互动”,描述 “搭建用户与党媒云之间的桥梁…”
每个分类 3 张卡片,4 个分类共 12 张卡片,仅用 6 行代码(3 个 @Builder 调用 × 4 个 Tab)实现。
五、卡片阴影 shadow 样式技巧
shadow是 ArkUI 提供的组件阴影属性,为卡片添加立体感和层次感。
5.1 shadow 参数详解
.shadow({radius:number,// 模糊半径(0=无模糊,越大越模糊)color:string,// 阴影颜色(含透明度)offsetX:number,// 水平偏移(正值向右,负值向左)offsetY:number// 垂直偏移(正值向下,负值向上)})5.2 三种阴影效果对比
| 级别 | 代码 | 视觉效果 | 适用场景 |
|---|---|---|---|
| 轻 | shadow({ radius: 2, color: '#0D000000', offsetY: 1 }) | 微弱阴影,若有若无 | 列表卡片、信息项 |
| 中 | shadow({ radius: 4, color: '#1A000000', offsetY: 2 }) | 明显阴影,有层次感 | 浮动按钮、弹出面板 |
| 重 | shadow({ radius: 8, color: '#33000000', offsetY: 4 }) | 深阴影,悬浮感强 | 模态框、底部弹窗 |
5.3 颜色透明度说明
| 十六进制前缀 | 透明度 | 十进制 |
|---|---|---|
#0D | ~5% | 13/255 |
#1A | ~10% | 26/255 |
#33 | ~20% | 51/255 |
#66 | ~40% | 102/255 |
本模块使用#0D000000(约 5% 透明度黑色),是最轻的阴影效果,适合列表中密集排列的卡片,避免阴影过于抢眼。
5.4 使用建议
- 密集列表(如本模块):使用轻量阴影(radius:2),避免视觉干扰
- 独立卡片(如详情页):使用中量阴影(radius:4),突出卡片存在感
- 浮动元素(如 FAB):使用重量阴影(radius:8),强调悬浮效果
- 避免在没有背景色差异的情况下使用阴影(白色卡片在白色背景上阴影不可见)
六、为何不需要 @Consumer(‘pageStack’)
6.1 当前状态
ServiceHome 没有子页面跳转——点击卡片不会跳转到详情页。因此不需要@Consumer('pageStack')获取 NavPathStack。
6.2 如需扩展
如果后续需要添加服务详情页,只需:
- 在
pages/下创建ServiceDetail.ets,使用 NavDestination - 在 ServiceHome 添加
@Consumer('pageStack') pageStack!: NavPathStack - 在 ServiceCard 的 onClick 中调用
pushPathByName('ServiceDetail', item) - 在 MainPage 的 pageMap 中注册路由
- 更新 Index.ets 导出
6.3 与其他模块对比
| 模块 | @Consumer | 子页面 | 原因 |
|---|---|---|---|
| news | ✅ | NewsDetail、NewsCategory | 需要点击新闻查看详情 |
| video | ✅ | VideoPlayer | 需要点击视频播放 |
| live | ❌ | 无 | 仅浏览列表 |
| personal | ✅ | LoginPage、MyComments | 需要登录和查看评论 |
| service | ❌ | 无 | 仅浏览卡片 |
七、扩展思路
7.1 添加 ServiceDetail 子页面
创建pages/ServiceDetail.ets,展示服务的详细内容:
@ComponentV2exportstruct ServiceDetail{@Consumer('pageStack')pageStack!:NavPathStack;@Localtitle:string='';aboutToAppear(){letparam=this.pageStack.getParamByName('ServiceDetail');if(param){this.title=paramasstring;}}build(){NavDestination(){...}.hideTitleBar(true)}}7.2 WebView 集成
使用 common 模块的CommonWeb组件加载在线服务页面:
import{CommonWeb}from'common';// 在 ServiceDetail 中CommonWeb({url:'https://service.example.com'})7.3 真实 API 对接
将模板字符串描述替换为 API 返回的真实数据,使用HttpUtil发起请求。
八、V2 装饰器使用汇总
| 装饰器 | 变量/位置 | 作用 |
|---|---|---|
@ComponentV2 | ServiceHome | V2 组件声明 |
@Local | currentIndex | Tab 选中索引 |
@Builder | ServiceCard | 可复用卡片模板 |
服务模块是 V2 装饰器使用最少的模块——没有@Consumer、没有@Param、没有@Event。
九、常见问题 Q&A
Q1: 为什么每个 Tab 的卡片内容结构相同?
A: 使用模板字符串`${item}平台`/`${item}矩阵`/`${item}互动`动态生成标题和描述,实现了"一套代码 × 4 个分类 = 12 张卡片"的效果。实际项目中每个分类的卡片结构和内容可能不同。
Q2:#0D000000是什么颜色格式?
A: 8 位十六进制颜色值,格式为#AARRGGBB。#0D是 Alpha(透明度,约 5%),000000是 RGB(纯黑)。ArkUI 支持 8 位颜色格式。
Q3: 为什么 ServiceHome 使用BG_COLOR背景而非Color.White?
A:StyleConstants.BG_COLOR是统一的页面背景色(通常是浅灰色#F5F5F5),与白色卡片形成对比,让卡片和 shadow 阴影更突出。如果背景也是白色,卡片阴影将不可见。
Q4: Scroll 组件的作用是什么?
A:Scroll让 TabContent 内容可滚动。当卡片数量超过屏幕高度时,用户可以上下滑动查看更多卡片。
十、小结
服务模块以 74 行源码实现了完整的服务入口页面,核心知识点包括:
- shadow 卡片阴影:
shadow({ radius, color, offsetY })为卡片添加立体感 - 模板字符串动态内容:
`${item}平台`用一套代码生成多个分类的卡片 - Tabs + ForEach:数据驱动的 Tab 动态生成模式
- BG_COLOR 背景 + 白色卡片:通过背景色对比突出卡片阴影效果
- 最精简模块结构:1 组件 + 74 行源码,适合快速理解 feature 模块的最小实现
服务模块虽然代码量最少,但shadow阴影和模板字符串动态内容是两个非常实用的技巧,在其他模块和实际项目中都有广泛应用。