鸿蒙ArkTS深度解析:构建星际资讯信息流应用的全栈架构与组件化实践
技术导读:本文将以一个完整的星际资讯信息流应用为案例,深入剖析鸿蒙HarmonyOS平台下ArkTS语言的组件化开发范式。从数据模型定义、状态管理机制、声明式UI构建、布局容器体系、事件驱动交互、模态弹窗架构,到多Tab页面路由体系,逐一拆解每一行代码背后的设计哲学与技术原理。读者将全面掌握
@Component、@State、@Builder、ForEach、Column、Row、Flex布局等ArkTS核心特性,理解鸿蒙声明式UI框架的运行机制,并获得在真实项目中落地这些技术的工程经验。
一、鸿蒙开发背景与ArkTS语言特性
鸿蒙HarmonyOS作为华为面向万物互联时代打造的分布式操作系统,其应用开发框架经历了从JavaUI到ArkTS声明式UI的重大演进。ArkTS是在TypeScript基础上扩展而来的编程语言,它保留了TypeScript的静态类型检查能力,同时融入了鸿蒙特有的声明式UI语法和状态管理原语。在ArkTS中,开发者不再需要像传统命令式框架那样手动操作DOM节点,而是通过声明组件树的结构和状态绑定关系,由框架自动驱动UI的更新与渲染。
ArkTS的声明式UI范式核心在于"状态驱动视图"的理念。当组件的状态变量发生变化时,框架会自动重新执行build函数,生成新的虚拟组件树,并与旧树进行Diff对比,最终只更新发生变化的最小UI单元。这种机制大幅减少了手动DOM操作带来的性能开销,也让代码逻辑更加可预测。在这一星际资讯应用中,大量的@State装饰器修饰的布尔变量控制着各类模态弹窗的显示与隐藏,当用户点击触发某个状态变更时,整个UI树会自动响应并渲染对应的模态层。
组件化开发思想是ArkTS架构的基石。通过@Component装饰器,开发者可以将一个复杂的页面拆分为多个高内聚、低耦合的独立单元。每个组件拥有自己的状态空间、构建方法和事件处理逻辑,组件之间通过属性传递和回调函数进行通信。这一星际资讯应用将整个界面拆分为六个独立的Tab组件(推荐、星闻、热搜、视频、频道、我的),每个Tab组件通过回调函数向父级组件传递用户操作意图,父级组件再统一调度模态弹窗的展示。这种"聪明组件-笨组件"的模式,使得每个子组件只关注自身的展示逻辑,而全局的状态协调集中在入口组件中完成。
ArkTS的类型系统提供了接口(interface)这一强大的数据建模工具。通过定义接口,开发者可以在编译阶段捕获类型不匹配的错误,避免运行时因数据结构不一致导致的崩溃。在这一应用中,定义了多达十二个接口来描述资讯条目、热搜话题、视频条目、频道、评论、创作者、通知、订阅项、快捷入口、专栏条目、直播间、快讯等数据模型,构建了一个完整的星际资讯领域模型。每个接口的字段都明确指定了类型,例如id: number表示数字类型的主键,title: string表示字符串类型的标题,这种严格的类型约束为整个应用的数据流提供了坚实的保障。
鸿蒙的布局系统提供了丰富的容器组件,包括Column(纵向排列)、Row(横向排列)、Stack(层叠布局)、Flex(弹性布局)等。这些容器组件可以相互嵌套,构建出任意复杂的界面结构。在这一应用中,Column被广泛用于垂直方向的元素堆叠,如资讯卡片的标题、作者信息、统计数据的垂直排列;Row则用于水平方向的元素并排,如标题与热度标签的左右排列、统计数据的三栏分布。通过layoutWeight属性,开发者可以灵活控制子元素在剩余空间中的分配比例,实现响应式布局效果。
鸿蒙的状态管理机制提供了多层级的装饰器体系。@State用于管理组件内部状态,当状态值变化时触发组件的重新渲染;@Prop用于父组件向子组件单向传递数据;@Link用于父子组件之间的双向状态同步;@Provide和@Consume则用于跨层级的依赖注入。在这一应用中,入口组件Index大量使用@State来管理十六个模态弹窗的显隐状态和九个当前选中数据项,这些状态构成了整个应用的交互中枢。
ArkTS的@Builder装饰器是该语言特有的UI构建单元。与@Component不同,@Builder方法不会创建独立的组件实例,而是在调用处内联展开。这意味着@Builder方法中引用的this指向的是其所属的组件实例,可以直接访问该组件的状态变量和方法。这一应用定义了多达十七个@Builder方法,用于构建各类模态弹窗(文章详情、评论、发布、话题、视频、频道、搜索、分享、作者、关注、通知、订阅、专栏、直播、会员、设置)以及底部导航项和模态遮罩层,这种设计使得模态弹窗的UI定义与主页面结构分离,代码组织更加清晰。
ForEach是ArkTS提供的列表渲染指令,它接收一个数据数组、一个子组件生成函数和一个键值生成函数。键值生成函数为每个列表项提供唯一标识,框架在数据变化时通过键值进行Diff对比,实现最小化更新。在这一应用中,ForEach被大量用于渲染资讯列表、热搜榜单、视频列表、频道网格、评论列表、创作者列表、通知列表、订阅列表、专栏列表、直播间列表和快讯列表,是构建动态列表UI的核心手段。
二、数据模型层:接口定义与类型安全
interface NewsItem {
id: number;
title: string;
author: string;
category: string;
reads: string;
comments: string;
cover: string;
hot: string;
time: string;
}

这段代码定义了资讯条目的数据接口NewsItem,它描述了每条星际新闻所包含的全部字段。id字段是数字类型的主键,用于在列表渲染时生成ForEach的唯一键值,以及在查找函数中精确定位某条资讯。title字段存储资讯标题文本,它会在列表卡片和详情弹窗中以不同字号呈现。author字段记录资讯的发布者名称,在卡片列表和详情页中作为来源信息展示。
category字段标识资讯的分类标签(如天文、航天、农垦等),在详情弹窗中以带背景色的小标签形式显示。reads和comments字段分别存储阅读量和评论数,设计为字符串类型而非数字类型,是因为这些数据已经格式化为带"万"单位的展示文本(如"1286万"),直接以字符串存储可以避免在UI层进行数字到展示文本的重复转换。
cover字段使用Emoji字符作为封面图的占位符(如🌌、🚀、🌾),这是一种轻量化的视觉呈现策略,在不需要真实图片资源的场景下快速构建视觉差异化。hot字段存储热度标记(如"爆"、“热”、“新”),当该字段不为空字符串时,在列表卡片上以红色或黄色小标签展示。time字段存储相对时间文本(如"12分钟前"),直接以展示格式存储,简化了UI层的格式化逻辑。
interface HotTopic {
rank: number;
title: string;
heat: string;
tag: string;
trend: string;
}
interface VideoItem {
id: number;
title: string;
up: string;
duration: string;
plays: string;
likes: string;
tag: string;
}
HotTopic接口定义了热搜话题的数据结构。rank字段是排名序号,用于在热搜榜单中确定话题的排列顺序和视觉样式(如第一名使用红色、使用更大的圆形背景)。heat字段存储热度值(如"6284万"),以字符串格式存储。tag字段存储热度标签(“爆"或"热”),trend字段存储热度趋势文本(如"↑ 231万"),展示热度相较上一周期的增长量。
VideoItem接口定义了视频条目的数据结构。up字段存储UP主名称(借用B站的术语,指视频上传者),duration存储时长文本(如"08:42"),plays和likes分别存储播放量和点赞数的格式化文本,tag存储视频类型标签(如"直播回放"、“科普”、“4K”)。这些字段的设计思路与NewsItem一脉相承,都采用字符串预格式化的策略,让数据层直接提供可展示的文本。
interface Channel {
id: number;
name: string;
desc: string;
subs: string;
hot: string;
color: string;
}
interface Comment {
id: number;
user: string;
content: string;
likes: string;
replies: string;
time: string;
level: string;
}

Channel接口定义了频道的数据结构。其中color字段是一个亮点设计,它存储十六进制颜色字符串(如"#8B5CF6"),用于在频道卡片中动态设置图标容器的背景色和订阅数字的文本颜色。这种将视觉属性数据化的做法,使得每个频道可以拥有独立的色彩标识,在不修改代码的情况下通过数据变更即可改变UI呈现。subs存储订阅人数文本,hot存储热度指数(数字字符串),desc存储频道的简短描述。
Comment接口定义了评论的数据结构。user存储评论者用户名,content存储评论正文,likes和replies分别存储点赞数和回复数的格式化文本。level字段存储用户等级标识(如"Lv8"),在评论卡片中以青色小标签展示,是用户身份层级的视觉信号。time字段存储评论的相对时间,帮助读者判断评论的新鲜度。
interface Creator {
id: number;
name: string;
field: string;
fans: string;
articles: string;
level: number;
}
interface Notice {
id: number;
type: string;
title: string;
detail: string;
time: string;
unread: string;
}

Creator接口定义了创作者的数据模型。值得注意的是level字段的类型是number而非string,这是因为它在UI中参与数值计算——作者弹窗中的"创作力"进度条宽度计算为this.selAuthor.level * 10 + '%',需要作为数值参与乘法运算。这是一个类型设计上的关键细节,说明了数据模型的字段类型不仅取决于数据本身的性质,还取决于它在UI层的使用方式。
Notice接口定义了通知消息的数据结构。type字段存储通知类型(“互动”、“关注”、“系统”),用于在通知列表中选择不同的图标(💬、📰、⚙️)和颜色。unread字段的设计很巧妙:它不是布尔类型,而是字符串类型——当值为"1"时表示未读,为空字符串""时表示已读。这种设计在UI渲染时通过n.unread !== ''的条件判断来决定是否显示未读小红点和标题字重加粗。
interface SubItem {
id: number;
name: string;
update: string;
category: string;
count: string;
}
interface QuickTab {
icon: string;
label: string;
color: string;
}
interface ColumnItem {
id: number;
title: string;
author: string;
price: string;
parts: string;
}
interface LiveRoom {
id: number;
title: string;
host: string;
viewers: string;
tag: string;
hot: string;
}
interface Breaking {
id: number;
title: string;
time: string;
level: string;
}
SubItem接口定义了订阅项数据,update字段存储最近更新的相对时间,count字段存储新文章数量的文本(如"3篇新文")。QuickTab接口定义了快捷入口的数据,icon存储Emoji图标,label存储入口名称,color存储该入口的主题色,与Channel接口的color字段设计思路一致。
ColumnItem接口定义了付费专栏的数据,price存储定价文本(如"36 星币"),parts存储章节数(如"48 讲")。LiveRoom接口定义了直播间数据,host存储主播名称,viewers存储观看人数文本,tag存储直播类型标签,hot存储热度标记。Breaking接口定义了快讯数据,level字段存储紧急级别(“红色”、“橙色”、“蓝色”),用于在快讯列表中选择不同颜色的圆点图标。
架构洞察:这十二个接口共同构成了一个完整的资讯应用领域模型。它们的设计遵循了"数据即展示文本"的原则——大部分数值字段都以字符串形式存储已格式化的文本,避免了UI层重复进行数字格式化的负担。唯一的例外是
Creator.level,因为它需要参与数值计算,这个细节体现了类型设计需要兼顾数据语义和使用场景的工程智慧。

三、Mock数据层:静态数据集的定义
const NEWS_FEED: NewsItem[] = [
{ id: 1, title: '引力波望远镜昨夜捕捉到织女星系异常脉冲', author: '星轨观察局', category: '天文', reads: '1286万', comments: '4.2万', cover: '🌌', hot: '爆', time: '12分钟前' },
{ id: 2, title: '移民船「晨昏线号」完成第 42 次轨道摆渡', author: '航运周报', category: '航天', reads: '863万', comments: '2.8万', cover: '🚀', hot: '热', time: '26分钟前' },
// ... 共10条
];
NEWS_FEED是一个NewsItem[]类型的常量数组,存储了十条星际新闻资讯。使用const关键字声明意味着该引用不可被重新赋值,但数组内容在理论上仍可被修改——不过在ArkTS的实践中,这类数据集通常被视为只读数据源。每条数据对象使用对象字面量语法创建,字段名与NewsItem接口定义一一对应,TypeScript的类型系统会在编译阶段验证每个字段的类型匹配性。
数据集的命名使用全大写加下划线的约定(NEWS_FEED、HOT_TOPICS、VIDEO_LIST等),这是区分常量数据与运行时变量的命名规范。这种命名约定在代码中具有视觉提示作用——当开发者看到全大写的标识符时,可以立即意识到这是一个不可变的静态数据源,而非可变的状态变量。
const HOT_TOPICS: HotTopic[] = [
{ rank: 1, title: '织女星系异常脉冲', heat: '6284万', tag: '爆', trend: '↑ 231万' },
{ rank: 2, title: '土卫六发光微生物', heat: '5103万', tag: '爆', trend: '↑ 188万' },
// ... 共10条
];
const VIDEO_LIST: VideoItem[] = [
{ id: 1, title: '实拍:织女星系脉冲波形实时监测', up: '星轨观察局', duration: '08:42', plays: '1246万', likes: '88.2万', tag: '直播回放' },
// ... 共10条
];
const CHANNELS: Channel[] = [
{ id: 1, name: '天文观测', desc: '星空深空每日要闻', subs: '1208万', hot: '94', color: '#8B5CF6' },
// ... 共8条
];

HOT_TOPICS数组定义了十个热搜话题,按热度排名从高到低排列。rank字段从1到10递增,与数组索引存在rank = index + 1的关系,但代码中显式存储rank而非依赖索引计算,这是为了在ForEach渲染时直接使用数据字段而非通过索引推导,提高了代码的可读性。
VIDEO_LIST数组定义了十个视频条目,CHANNELS数组定义了八个频道。注意CHANNELS中每条数据的color字段都使用了不同的十六进制颜色值,这些颜色值在UI渲染时会被传递给容器组件的backgroundColor属性和文本组件的fontColor属性,实现了数据驱动的色彩主题化。这种设计意味着如果需要新增一个频道,只需在数据集中添加一条带颜色值的数据,无需修改任何组件代码。
const COMMENTS: Comment[] = [
{ id: 1, user: '轨道漫游者', content: '脉冲波形图第三段明显有周期性,等官方论文了', likes: '8.2万', replies: '1286', time: '18分钟前', level: 'Lv8' },
// ... 共8条
];
const CREATORS: Creator[] = [
{ id: 1, name: '星轨观察局', field: '天文观测', fans: '986万', articles: '1242', level: 9 },
// ... 共8条
];
const NOTICES: Notice[] = [
{ id: 1, type: '互动', title: '轨道漫游者 回复了你的评论', detail: '"同意,第三段波形确实反常"', time: '5分钟前', unread: '1' },
// ... 共6条
];
COMMENTS数组定义了八条评论数据,每条包含用户名、评论内容、点赞数、回复数、时间和等级。CREATORS数组定义了八位创作者,其中level字段是数字类型(如9、8、7),这是所有接口中唯一一个值为纯数字而非字符串的数值字段——它在作者弹窗中参与进度条宽度计算的数学运算。
NOTICES数组定义了六条通知消息,unread字段为"1"时表示未读,为空字符串"“时表示已读。前两条通知的unread为"1”,在UI上会显示未读红点并加粗标题,后四条为已读状态。这种以字符串而非布尔值表示二值状态的做法,虽然从类型设计的角度看不够纯粹,但在ArkTS的条件渲染中,n.unread !== ''的判断方式非常直观且与字符串数据集的自然格式相匹配。
const QUICK_TABS: QuickTab[] = [
{ icon: '🌌', label: '深空直播', color: '#8B5CF6' },
{ icon: '🔭', label: '观测预约', color: '#22D3EE' },
// ... 共8条
];
const COLUMNS: ColumnItem[] = [
{ id: 1, title: '深空观测手册·脉冲篇', author: '星轨观察局', price: '36 星币', parts: '48 讲' },
// ... 共5条
];
const LIVE_ROOMS: LiveRoom[] = [
{ id: 1, title: '织女星系脉冲波形实时监测', host: '星轨观察局', viewers: '48.2万', tag: '监测', hot: '爆' },
// ... 共6条
];
const BREAKING: Breaking[] = [
{ id: 1, title: '快讯:织女星系脉冲再次复现,强度提升 12%', time: '刚刚', level: '红色' },
// ... 共6条
];
QUICK_TABS数组定义了八个快捷入口项,分为左右两组各四个,通过getQuickLeft()和getQuickRight()函数获取。COLUMNS数组定义了五个付费专栏,分为左三右二的两组。LIVE_ROOMS数组定义了六个直播间,分为左右各三个。BREAKING数组定义了六条快讯,level字段使用中文颜色名称(“红色”、“橙色”、“蓝色”)而非颜色值字符串,在UI渲染时通过三元运算符映射为对应的Emoji圆点图标。
设计要点:这些静态数据集构成了应用的"数据假体"。在真实项目中,它们会被替换为从网络请求获取的动态数据,但由于ArkTS的声明式UI架构是数据驱动的,从静态数据切换到动态数据只需改变数据源的获取方式,而所有渲染组件的代码无需任何修改。这是声明式UI架构的核心优势之一。
四、数据访问层:切片函数与查找函数
function getFeedLeft(): NewsItem[] {
return NEWS_FEED.slice(0, 5);
}
function getFeedRight(): NewsItem[] {
return NEWS_FEED.slice(5, 10);
}
function getFeedCount(): number {
return NEWS_FEED.length;
}

这三个函数构成了资讯数据的基本访问接口。getFeedLeft()使用JavaScript数组的slice(0, 5)方法,返回NEWS_FEED数组的前五条数据,用于在推荐Tab的信息流左列渲染。getFeedRight()使用slice(5, 10)返回后五条数据,用于右列渲染。这种将数据切分为左右两组的设计,是为了实现瀑布流双列布局——左列和右列各自独立渲染,视觉上呈现错落有致的卡片排列效果。
getFeedCount()返回数据数组的长度,用于在分区标题中显示资讯总数(如"星闻信息流 (10)")。将数组长度封装为函数而非直接在UI中使用NEWS_FEED.length,是为了抽象数据访问入口——当数据源从静态数组切换为动态API时,只需修改函数内部的实现,所有调用处无需改变。
function getTopFeed(): NewsItem[] {
return NEWS_FEED.slice(0, 3);
}
function getHotTop(): HotTopic[] {
return HOT_TOPICS.slice(0, 3);
}
function getHotRest(): HotTopic[] {
return HOT_TOPICS.slice(3, 10);
}
function getHotCount(): number {
return HOT_TOPICS.length;
}

getTopFeed()返回前三条资讯,用于在频道弹窗和作者弹窗的"频道热文"和"近期代表作"列表中渲染。getHotTop()返回热搜榜前三名,这三条数据在热搜Tab中以特殊的大卡片样式展示(带渐变背景和金边装饰)。getHotRest()返回第4到第10名,以标准列表项样式展示。getHotCount()返回热搜总数。
这种将数据按展示需求切片的设计,使得同一数据源可以在不同场景中以不同的子集呈现。例如热搜数据在推荐Tab中只展示前三名(getHotTop()),而在热搜Tab中则展示全部十名(前三名用getHotTop()获取特殊展示,后七名用getHotRest()获取标准展示)。
function getVideoLeft(): VideoItem[] {
return VIDEO_LIST.slice(0, 5);
}
function getVideoRight(): VideoItem[] {
return VIDEO_LIST.slice(5, 10);
}
function getTopVideos(): VideoItem[] {
return VIDEO_LIST.slice(0, 4);
}
function getVideoCount(): number {
return VIDEO_LIST.length;
}
视频数据的切片函数遵循相同的模式。getTopVideos()返回前四条视频,用于视频Tab顶部的横向滚动推荐区域。这种横向滚动的"精选推荐"区域是资讯类应用常见的UI模式,通过Scroll容器的scrollable(ScrollDirection.Horizontal)属性实现水平滚动。
function getChannelLeft(): Channel[] {
return CHANNELS.slice(0, 4);
}
function getChannelRight(): Channel[] {
return CHANNELS.slice(4, 8);
}
function getTopComments(): Comment[] {
return COMMENTS.slice(0, 4);
}
function getCommentList(): Comment[] {
return COMMENTS;
}
getTopComments()返回前四条评论,用于星闻Tab的"热评速览"区域。getCommentList()返回全部评论,用于评论弹窗的完整列表。getCommentCount()返回评论总数,用于弹窗标题中展示(如"评论区 8条")。
function getCreatorLeft(): Creator[] {
return CREATORS.slice(0, 4);
}
function getCreatorRight(): Creator[] {
return CREATORS.slice(4, 8);
}
function getTopCreators(): Creator[] {
return CREATORS.slice(0, 5);
}
function getCreatorCount(): number {
return CREATORS.length;
}
创作者数据的切片函数同样遵循左右分组模式。getTopCreators()返回前五位创作者,用于在热搜Tab的"话题主导者"、视频Tab的"人气UP主"和我的Tab的"我关注的作者"中以横向排列的圆形头像列表形式展示。
function findFeedByTitle(title: string): NewsItem {
for (let i = 0; i < NEWS_FEED.length; i++) {
if (NEWS_FEED[i].title === title) {
return NEWS_FEED[i];
}
}
return NEWS_FEED[0];
}
这是一个典型的线性查找函数,通过标题文本在NEWS_FEED数组中查找对应的资讯条目。使用传统的for循环遍历数组,逐条比较title字段的值与传入参数。当找到匹配项时立即返回,如果遍历结束仍未找到则返回数组的第一条作为默认值。这种"找不到返回默认值"的设计避免了返回undefined可能导致的空指针异常,保证了UI渲染时始终有有效数据。
这种查找函数的存在是因为子组件通过回调函数向父组件传递的是数据的标识信息(标题文本),而非数据对象本身。父组件收到标题后,通过查找函数在数据集中定位完整的数据对象,再将其赋值给状态变量(如this.selArticle),触发详情弹窗的渲染。这种"传递标识而非对象"的通信模式,降低了组件间数据传递的耦合度。
function findTopicByTitle(title: string): HotTopic {
for (let i = 0; i < HOT_TOPICS.length; i++) {
if (HOT_TOPICS[i].title === title) {
return HOT_TOPICS[i];
}
}
return HOT_TOPICS[0];
}
function findVideoByTitle(title: string): VideoItem {
for (let i = 0; i < VIDEO_LIST.length; i++) {
if (VIDEO_LIST[i].title === title) {
return VIDEO_LIST[i];
}
}
return VIDEO_LIST[0];
}
function findChannelByName(name: string): Channel {
for (let i = 0; i < CHANNELS.length; i++) {
if (CHANNELS[i].name === name) {
return CHANNELS[i];
}
}
return CHANNELS[0];
}
这些查找函数遵循完全一致的代码模式:遍历数组、比较字段值、找到则返回、未找到返回首条。它们分别为热搜话题(按标题查找)、视频(按标题查找)、频道(按名称查找)提供查找能力。注意频道查找使用的是name字段而非title,因为Channel接口没有title字段,其标识字段是name。
function findCreatorByName(name: string): Creator {
for (let i = 0; i < CREATORS.length; i++) {
if (CREATORS[i].name === name) {
return CREATORS[i];
}
}
return CREATORS[0];
}
function findSubByName(name: string): SubItem {
for (let i = 0; i < SUBS.length; i++) {
if (SUBS[i].name === name) {
return SUBS[i];
}
}
return SUBS[0];
}
function findColumnByTitle(title: string): ColumnItem {
for (let i = 0; i < COLUMNS.length; i++) {
if (COLUMNS[i].title === title) {
return COLUMNS[i];
}
}
return COLUMNS[0];
}
function findLiveByTitle(title: string): LiveRoom {
for (let i = 0; i < LIVE_ROOMS.length; i++) {
if (LIVE_ROOMS[i].title === title) {
return LIVE_ROOMS[i];
}
}
return LIVE_ROOMS[0];
}
function findNoticeByTitle(title: string): Notice {
for (let i = 0; i < NOTICES.length; i++) {
if (NOTICES[i].title === title) {
return NOTICES[i];
}
}
return NOTICES[0];
}
这组查找函数覆盖了创作者(按名称)、订阅项(按名称)、专栏(按标题)、直播间(按标题)、通知(按标题)的查找需求。每一个函数都是相同的算法结构,只是操作的数据集和比较字段不同。这种高度一致的代码模式反映了应用中统一的"通过标识查找完整数据"的交互范式。
工程思考:这些查找函数虽然结构简单,但在应用架构中承担着关键的"数据路由"职责。当用户在某个Tab页面点击一条资讯卡片时,子组件回调传递的是卡片的标题文本,父级组件
Index收到回调后调用对应的查找函数,将标题文本"路由"到完整的数据对象,再赋值给@State状态变量触发模态弹窗。这构成了"点击-回调-查找-状态更新-弹窗渲染"的完整交互链路。
五、入口组件:状态管理与路由调度
@Entry
@Component
struct Index {
@State tabIdx: number = 0;
@State showArticle: boolean = false;
@State showComment: boolean = false;
@State showPublish: boolean = false;
@State showTopic: boolean = false;
@State showVideo: boolean = false;
@State showChannel: boolean = false;
@State showSearch: boolean = false;
@State showShare: boolean = false;
@State showAuthor: boolean = false;
@State showFollow: boolean = false;
@State showNotice: boolean = false;
@State showSub: boolean = false;
@State showColumn: boolean = false;
@State showLive: boolean = false;
@State showVip: boolean = false;
@State showSettings: boolean = false;
@Entry装饰器标记此组件为应用的入口页面组件,一个ArkTS页面有且仅有一个@Entry组件。@Component装饰器将Index结构体声明为一个自定义组件,使其可以拥有build()方法和@Builder方法。struct关键字是ArkTS特有的,它类似于TypeScript中的class但语义更接近于值类型结构体——ArkTS的自定义组件使用struct而非class声明。
@State装饰器声明的是组件内部的响应式状态变量。当这些变量的值发生变化时,框架会自动触发组件的重新构建(重新执行build()函数或相关的@Builder方法),并精确更新受影响的UI部分。tabIdx变量是整个应用的核心路由状态,它决定了当前展示哪个Tab页面(0=推荐、1=星闻、2=热搜、3=视频、4=频道、5=我的)。
十六个show*布尔变量控制着十六个模态弹窗的显示与隐藏。这种"每个弹窗一个布尔状态"的设计简单直观——当showArticle为true时显示文章详情弹窗,为false时隐藏。多个弹窗状态可以同时为true,实现弹窗叠加(如在文章弹窗中点击"写评论"会同时打开评论弹窗,两层弹窗叠加显示)。
@State selArticle: NewsItem = NEWS_FEED[0];
@State selTopic: HotTopic = HOT_TOPICS[0];
@State selVideo: VideoItem = VIDEO_LIST[0];
@State selChannel: Channel = CHANNELS[0];
@State selAuthor: Creator = CREATORS[0];
@State selSub: SubItem = SUBS[0];
@State selColumn: ColumnItem = COLUMNS[0];
@State selLive: LiveRoom = LIVE_ROOMS[0];
@State selNotice: Notice = NOTICES[0];
@State likeNum: number = 2386;
@State followNum: number = 56;
这组@State变量存储的是当前选中的数据对象,用于在各模态弹窗中展示详情。每个变量在声明时被初始化为对应数据集的第一条,确保弹窗在首次打开时就有有效数据可展示。当用户点击某个列表项时,对应的sel*变量会被更新为该项的数据对象,然后show*变量被设为true,触发弹窗渲染并展示新选中的数据。
likeNum和followNum是两个数值类型的状态变量,分别存储点赞数和关注数。likeNum在文章弹窗中展示,followNum在作者弹窗的"关注"按钮中展示。这两个变量的特殊之处在于它们会在用户交互中被递增(如点击关注按钮时this.followNum = this.followNum + 1),直接反映用户的操作结果。
@Builder
modalOverlay(onClose: () => void) {
Column() {
}
.width('100%')
.height('100%')
.backgroundColor('rgba(5,2,12,0.66)')
.onClick(() => {
onClose();
})
}
modalOverlay是一个@Builder方法,用于生成模态弹窗的遮罩层。它接收一个onClose回调函数作为参数,当遮罩层被点击时调用该回调。遮罩层是一个空内容的Column容器,设置了100%宽高和半透明的深色背景(rgba(5,2,12,0.66)表示66%不透明度的深蓝黑色),覆盖在整个页面上方,阻挡用户对底层的交互。
这种"遮罩层+弹窗"的模态交互模式是移动端应用的标准设计。遮罩层的半透明背景让用户可以隐约看到底层内容,同时明确告知当前处于模态状态。点击遮罩层关闭弹窗是最常见的交互约定——用户通过点击弹窗外的区域表达"关闭"意图,onClose回调接收这一意图并执行状态变更。
六、入口组件build方法:页面骨架构建
build() {
Column() {
Column() {
Row() {
Text('⭐ 星头条')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
Row() {
Text('🔍 搜索星闻·作者·频道')
.fontSize(12)
.fontColor('#9D8FC2')
}
.layoutWeight(1)
.height(32)
.borderRadius(16)
.backgroundColor('rgba(255,255,255,0.1)')
.margin({ left: 12, right: 12 })
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showSearch = true;
})
Text('✍️')
.fontSize(20)
.onClick(() => {
this.showPublish = true;
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 10 })
build()方法是每个@Component组件必须实现的方法,它返回组件的UI结构。Index组件的build()方法最外层是一个全屏的Column容器,设置了深色背景色#0D0718(近乎黑色的深紫蓝色),为整个应用奠定了深色主题的基调。
头部区域是一个嵌套的Column,内层包含两行。第一行是一个Row,横向排列了应用标题"⭐ 星头条"、搜索框和发布按钮。标题使用Text组件展示,设置了20px字号、粗体字重和浅紫色文本颜色#F5F0FF。搜索框本身也是一个Row容器,通过layoutWeight(1)占据标题和发布按钮之间的剩余空间,设置了32px高度、16px圆角和10%不透明度的白色背景,模拟搜索输入框的外观。点击搜索框触发this.showSearch = true打开搜索弹窗。
发布按钮是一个Text组件显示"✍️"Emoji,设置了20px字号和点击事件——点击触发this.showPublish = true打开发布弹窗。这里搜索框和发布按钮都使用了Emoji字符作为图标,这是一种轻量化的图标方案,无需引入图片资源即可实现视觉表达。
Column与Row的布局哲学:
Column是ArkTS中最基础的垂直布局容器,它将其子组件从上到下依次排列。Row则是水平布局容器,从左到右排列子组件。这两个容器的组合使用是构建绝大多数UI布局的基础——在这一头部区域中,外层Column负责垂直堆叠两行内容,内层Row负责水平排列标题、搜索框和按钮。这种"列中嵌行、行中嵌列"的嵌套模式贯穿了整个应用的UI构建。
Row() {
Text('COSMIC NEWS')
.fontSize(10)
.fontColor('#7C6BA8')
.letterSpacing(2)
Row() {
}
.layoutWeight(1)
Text('今日阅读 ' + this.readMinutes() + ' 分钟')
.fontSize(10)
.fontColor('#9D8FC2')
}
.width('100%')
.padding({ left: 16, right: 16, top: 6, bottom: 8 })
}
.width('100%')
.linearGradient({
angle: 160,
colors: [['#1B0F33', 0.0], ['##2A1752', 0.55], ['#4C2E8C', 1.0]]
})
头部区域的第二行同样是一个Row,包含左侧的"COSMIC NEWS"英文副标题、中间的空白占位Row(通过layoutWeight(1)撑开空间)和右侧的"今日阅读"统计。letterSpacing(2)属性为英文文本设置了2像素的字间距,增强了排版的高级感。
整个头部Column容器使用了linearGradient属性设置线性渐变背景。angle: 160指定渐变角度为160度(接近从上到下),colors数组定义了三个颜色停止点:起始位置(0.0)为#1B0F33(深紫色),中间位置(0.55)为#2A1752(中紫色),结束位置(1.0)为#4C2E8C(亮紫色)。这种渐变背景为头部区域赋予了深邃的星空感,与"星际头条"的主题相呼应。
linearGradient是ArkTS提供的高级视觉属性,它通过GPU加速渲染渐变效果,性能优于使用图片背景。angle参数以度为单位,0度表示从下到上,90度表示从左到右,180度表示从上到下。colors参数是一个数组,每个元素是[颜色值, 位置]的元组,位置值范围为0.0到1.0。
Column() {
Scroll() {
Column() {
if (this.tabIdx === 0) {
HomeTab({
onFeed: (title: string) => {
this.selArticle = findFeedByTitle(title);
this.showArticle = true;
},
onQuick: (i: number) => {
if (i === 0) {
this.selLive = LIVE_ROOMS[0];
this.showLive = true;
} else if (i === 1) {
this.showSearch = true;
} else if (i === 2) {
this.selTopic = HOT_TOPICS[0];
this.showTopic = true;
} else if (i === 3) {
this.selNotice = NOTICES[0];
this.showNotice = true;
} else if (i === 4) {
this.selVideo = VIDEO_LIST[0];
this.showVideo = true;
} else if (i === 5) {
this.selChannel = CHANNELS[0];
this.showChannel = true;
} else if (i === 6) {
this.showPublish = true;
} else {
this.selNotice = NOTICES[0];
this.showNotice = true;
}
},
onAuthor: (name: string) => {
this.selAuthor = findCreatorByName(name);
this.showAuthor = true;
},
onSub: (name: string) => {
this.selSub = findSubByName(name);
this.showSub = true;
}
})
内容区域的主体是一个Scroll容器内嵌Column,用于承载条件渲染的Tab组件。ArkTS的if/else if/else条件渲染语法允许在build()函数中根据状态值动态选择渲染哪个组件。当this.tabIdx === 0时渲染HomeTab组件,=== 1时渲染StarTab,以此类推。
HomeTab组件通过属性传递接收四个回调函数:onFeed、onQuick、onAuthor、onSub。这些回调函数是子组件向父组件通信的桥梁。当用户在HomeTab中点击某条资讯时,HomeTab调用this.onFeed(title),传入资讯标题。父组件Index接收到回调后,执行this.selArticle = findFeedByTitle(title)查找完整数据对象并更新选中状态,然后设置this.showArticle = true打开文章详情弹窗。
onQuick回调处理快捷入口的点击事件。参数i是快捷入口的索引(0-7),对应八个快捷入口。通过if/else if链判断i的值,分别打开不同的弹窗或页面。例如i === 0打开直播弹窗,i === 1打开搜索弹窗,i === 2打开话题弹窗等。这种集中式的快捷入口路由设计,使得所有的导航逻辑都集中在入口组件中,便于维护和修改。
} else if (this.tabIdx === 1) {
StarTab({
onFeed: (title: string) => {
this.selArticle = findFeedByTitle(title);
this.showArticle = true;
},
onAuthor: (name: string) => {
this.selAuthor = findCreatorByName(name);
this.showAuthor = true;
},
onColumn: (title: string) => {
this.selColumn = findColumnByTitle(title);
this.showColumn = true;
},
onLive: (title: string) => {
this.selLive = findLiveByTitle(title);
this.showLive = true;
},
onComment: () => {
this.showComment = true;
}
})
} else if (this.tabIdx === 2) {
HotTab({
onTopic: (title: string) => {
this.selTopic = findTopicByTitle(title);
this.showTopic = true;
},
onFeed: (title: string) => {
this.selArticle = findFeedByTitle(title);
this.showArticle = true;
},
onAuthor: (name: string) => {
this.selAuthor = findCreatorByName(name);
this.showAuthor = true;
}
})
StarTab(星闻Tab)传递了五个回调:onFeed、onAuthor、onColumn、onLive、onComment。与HomeTab的回调相比,多了onColumn(专栏点击)、onLive(直播点击)和onComment(评论入口点击)三个回调。每个回调都遵循相同的模式:接收标识参数 → 查找数据 → 更新选中状态 → 打开对应弹窗。
HotTab(热搜Tab)传递了三个回调:onTopic(话题点击)、onFeed(快讯点击)和onAuthor(创作者点击)。注意onFeed回调中传入的参数虽然来自快讯列表,但查找的是NEWS_FEED数据集——因为快讯和资讯在数据模型上是关联的,点击快讯会展示对应的资讯详情。
} else if (this.tabIdx === 3) {
VideoTab({
onVideo: (title: string) => {
this.selVideo = findVideoByTitle(title);
this.showVideo = true;
},
onLive: (title: string) => {
this.selLive = findLiveByTitle(title);
this.showLive = true;
},
onAuthor: (name: string) => {
this.selAuthor = findCreatorByName(name);
this.showAuthor = true;
}
})
} else if (this.tabIdx === 4) {
ChannelTab({
onChannel: (name: string) => {
this.selChannel = findChannelByName(name);
this.showChannel = true;
},
onSub: (name: string) => {
this.selSub = findSubByName(name);
this.showSub = true;
},
onColumn: (title: string) => {
this.selColumn = findColumnByTitle(title);
this.showColumn = true;
}
})
} else {
MineTab({
onSub: (name: string) => {
this.selSub = findSubByName(name);
this.showSub = true;
},
onNotice: (title: string) => {
this.selNotice = findNoticeByTitle(title);
this.showNotice = true;
},
onVip: () => {
this.showVip = true;
},
onSettings: () => {
this.showSettings = true;
},
onShare: () => {
this.showShare = true;
},
onAuthor: (name: string) => {
this.selAuthor = findCreatorByName(name);
this.showAuthor = true;
},
onColumn: (title: string) => {
this.selColumn = findColumnByTitle(title);
this.showColumn = true;
}
})
}
VideoTab传递三个回调:onVideo、onLive、onAuthor。ChannelTab传递三个回调:onChannel、onSub、onColumn。MineTab(我的Tab)传递了多达七个回调:onSub、onNotice、onVip、onSettings、onShare、onAuthor、onColumn,是所有Tab中回调最多的。这是因为"我的"页面功能最丰富,涵盖了订阅管理、消息通知、会员服务、设置、分享、作者详情和专栏管理等多个入口。
注意onVip、onSettings、onShare这三个回调没有接收参数——它们不需要查找数据,只是纯粹的"打开某个弹窗"的信号。这与其他回调"接收标识→查找数据→打开弹窗"的模式不同,体现了不同类型交互的差异化处理。
组件通信模式:这个应用展示了一种经典的ArkTS组件通信模式——“回调函数传递意图”。父组件将回调函数作为属性传递给子组件,子组件在用户交互时调用回调传递意图信息(如标题、名称、索引),父组件接收回调后执行状态更新。这种模式保持了子组件的无状态性(子组件本身不持有弹窗状态,只负责触发回调),将所有状态管理集中在入口组件中,是"聪明父组件-笨子组件"架构的典型实现。
七、底部导航栏构建
Row() {
this.bottomTabItem('🏠', '推荐', 0)
this.bottomTabItem('🌟', '星闻', 1)
this.bottomTabItem('🔥', '热搜', 2)
this.bottomTabItem('🎬', '视频', 3)
this.bottomTabItem('📡', '频道', 4)
this.bottomTabItem('👤', '我的', 5)
}
.width('100%')
.height(56)
.backgroundColor('rgba(24,12,46,0.98)')
.borderRadius({ topLeft: 18, topRight: 18 })
底部导航栏是一个Row容器,固定高度56px,设置了98%不透明度的深紫蓝色背景和顶部圆角。六个bottomTabItem通过this.bottomTabItem(...)调用@Builder方法生成,每个接收图标、标签和索引三个参数。@Builder方法调用使用this.前缀,因为bottomTabItem是当前组件的成员方法。
底部导航栏的视觉设计体现了移动端应用的常见模式:顶部圆角让导航栏与内容区产生视觉分离感,半透明背景(98%不透明度而非完全不透明)在保持高可读性的同时保留了一丝通透感。56px的高度是底部导航的常见尺寸,既保证了触摸区域的足够大小,又不会过多占用屏幕空间。
@Builder
bottomTabItem(icon: string, label: string, idx: number) {
Column({ space: 3 }) {
Text(icon)
.fontSize(20)
.scale(this.tabIdx === idx ? { x: 1.18, y: 1.18 } : { x: 1, y: 1 })
.animation({ duration: 220, curve: Curve.EaseOut })
Text(label)
.fontSize(10)
.fontColor(this.tabIdx === idx ? '#A78BFA' : '#6B5B8A')
.fontWeight(this.tabIdx === idx ? FontWeight.Bold : FontWeight.Normal)
}
.width('16.6%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.tabIdx = idx;
})
}
bottomTabItem是一个@Builder方法,接收三个参数:icon(图标Emoji)、label(标签文本)、idx(Tab索引)。它生成一个Column容器,纵向排列图标和标签文本,间距为3px。
图标Text组件使用scale属性实现选中态的放大效果:当this.tabIdx === idx(当前选中Tab等于此项索引)时,缩放比例为1.18倍(x和y方向均放大18%),否则为1倍(原始大小)。animation属性为缩放变化添加了220毫秒的动画过渡,使用Curve.EaseOut缓动曲线——这种曲线开始快、结束慢,适合放大动画的视觉感受。
标签Text组件根据选中状态切换文本颜色和字重:选中时为亮紫色#A78BFA且加粗,未选中时为暗紫色#6B5B8A且正常字重。这种双态视觉差异让用户可以清晰识别当前所在的Tab。
整个Column容器设置了width('16.6%')(约六分之一屏幕宽度,对应六个Tab项),justifyContent(FlexAlign.Center)使内容居中对齐。点击事件设置this.tabIdx = idx,触发条件渲染切换Tab内容。
FlexAlign是ArkTS的弹性布局对齐枚举。FlexAlign.Center表示主轴方向居中对齐。在Column中,主轴是垂直方向,因此FlexAlign.Center使子元素垂直居中。其他常用值包括FlexAlign.Start(顶部对齐)、FlexAlign.End(底部对齐)、FlexAlign.SpaceBetween(两端对齐,中间均分)、FlexAlign.SpaceAround(每个元素两侧等距)、FlexAlign.SpaceEvenly(所有间隙完全相等)。
readMinutes(): number {
return 42 + this.tabIdx * 3;
}
readMinutes是一个普通的成员方法(非@Builder),返回一个基于当前Tab索引计算的阅读分钟数。当tabIdx为0时返回42,为1时返回45,为5时返回57。这个方法在头部区域的Text('今日阅读 ' + this.readMinutes() + ' 分钟')中被调用,由于this.tabIdx是@State变量,当用户切换Tab时,readMinutes()的返回值也会变化,头部显示的阅读时间会随之更新。这是状态驱动视图的一个微妙体现——虽然readMinutes()不是@State变量,但它依赖的this.tabIdx是@State,因此框架会在tabIdx变化时重新评估引用了readMinutes()的Text组件。
八、模态弹窗体系:文章详情弹窗
@Builder
articleModal() {
Column() {
Column() {
Column() {
Text(this.selArticle.cover)
.fontSize(56)
}
.width('100%')
.height(150)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [['#4C2E8C', 0.0], ['#8B5CF6', 1.0]]
})
.justifyContent(FlexAlign.Center)
articleModal是文章详情弹窗的@Builder方法。外层是一个全屏Column,设置justifyContent(FlexAlign.Center)使弹窗内容垂直居中。内层Column宽度为86%,设置了20px圆角和深色背景#1E1136,alignItems(HorizontalAlign.Start)使内部元素左对齐。
弹窗顶部是一个封面区域——一个Column容器,设置了150px高度、16px圆角和从深紫到亮紫的135度线性渐变背景。居中放置了一个56px字号的Text组件,显示当前选中资讯的Emoji封面字符。由于Emoji在56px字号下会占据较大空间,配合渐变背景形成了一个具有视觉冲击力的封面头图。
HorizontalAlign是ArkTS的交叉轴对齐枚举。在Column中,主轴是垂直方向,交叉轴是水平方向,因此HorizontalAlign.Start使子元素左对齐。其他值包括HorizontalAlign.Center(水平居中)和HorizontalAlign.End(右对齐)。
Text(this.selArticle.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 12 })
Row({ space: 6 }) {
Text(this.selArticle.category)
.fontSize(10)
.fontColor('#A78BFA')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('rgba(139,92,246,0.18)')
Text(this.selArticle.author)
.fontSize(11)
.fontColor('#9D8FC2')
Text(this.selArticle.time)
.fontSize(10)
.fontColor('#6B5B8A')
}
.margin({ top: 8 })
标题区域展示资讯标题,18px粗体字、浅紫色文本。下方是一个Row容器,横向排列分类标签、作者名和时间。分类标签使用10px字号、紫色文本,带有18%不透明度的紫色背景和10px圆角,形成了一个pill形态的小标签。作者名和时间使用较小的字号和递减的文本颜色亮度,形成了清晰的信息层级。
Row({ space: 6 })的space参数设置了子元素之间的固定间距为6px。这与margin方式的间距不同——space参数由布局系统统一分配,更加简洁。Row和Column都支持space参数,是ArkTS布局容器的基础特性。
Text('引力波观测网络在昨夜例行巡天中捕捉到一组来自织女星系方向的窄带脉冲信号,周期稳定在 0.42 秒,强度较上一次记录提升 12%。研究组已排除设备热噪声与已知脉冲星数据库匹配项,正在申请深空阵列进行交叉验证……')
.fontSize(13)
.fontColor('#C4B5E0')
.lineHeight(22)
.margin({ top: 12 })
文章正文使用了一个固定的占位文本,13px字号、浅紫色文本#C4B5E0,行高22px。lineHeight(22)设置了文本行高为22像素,相比13px的字号,行高比约为1.7,提供了舒适的阅读间距。这里使用固定文本而非动态数据,是因为在实际应用中正文通常需要富文本渲染或WebView展示,在此Demo中以简化方式呈现。
Row() {
Column() {
Text(this.selArticle.reads)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#22D3EE')
Text('阅读量')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text(this.selArticle.comments)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#F472B6')
Text('评论数')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text(this.likeNum + '')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FBBF24')
Text('点赞数')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('rgba(139,92,246,0.12)')
.margin({ top: 14 })
统计数据区域是一个Row容器,内含三个Column,每个Column通过layoutWeight(1)等分宽度。每个统计单元展示一个数值和对应的标签:阅读量(青色#22D3EE)、评论数(粉色#F472B6)、点赞数(黄色#FBBF24)。三色编码让不同维度的数据具有视觉区分度。
点赞数使用this.likeNum + ''将数值转换为字符串,这是因为Text组件的参数需要字符串类型。likeNum是@State变量,初始值2386,当用户在弹窗中点击"写评论"后会触发评论弹窗,虽然在此Demo中点赞数不会因弹窗内操作而变化,但@State的声明确保了它具备响应式更新的能力。
Row({ space: 10 }) {
Button() {
Text('💬 写评论')
.fontSize(13)
.fontColor('#F5F0FF')
.fontWeight(FontWeight.Medium)
}
.layoutWeight(1)
.height(42)
.borderRadius(21)
.linearGradient({
angle: 90,
colors: [['#8B5CF6', 0.0], ['#22D3EE', 1.0]]
})
.onClick(() => {
this.showComment = true;
})
Button() {
Text('↗ 分享')
.fontSize(13)
.fontColor('#A78BFA')
.fontWeight(FontWeight.Medium)
}
.layoutWeight(1)
.height(42)
.borderRadius(21)
.backgroundColor('rgba(139,92,246,0.16)')
.onClick(() => {
this.showShare = true;
})
}
.width('100%')
.margin({ top: 14 })
底部操作区是两个Button组件。"写评论"按钮使用了从紫到青的90度线性渐变背景,圆角21px(高度42px的一半,形成完全圆形端),点击触发this.showComment = true打开评论弹窗。"分享"按钮使用半透明紫色背景,点击触发this.showShare = true打开分享弹窗。
Button组件是ArkTS的内置交互组件。它支持两种使用方式:Button('文本')的简写形式和Button() { Text('文本') }的自定义内容形式。这里使用的是后者,因为需要对按钮内的文本进行字号、颜色和字重的自定义设置。Button组件默认有点击反馈效果(涟漪动画),提供了基础的交互反馈。
整个弹窗的最外层设置了transition(TransitionEffect.OPACITY.animation({ duration: 200 })),这定义了弹窗的进场和退场动画——200毫秒的透明度过渡效果。TransitionEffect.OPACITY表示使用透明度变化作为转场效果,配合animation修饰指定动画时长。当弹窗的show*状态从false变为true时,弹窗以渐显方式出现;从true变为false时以渐隐方式消失。
九、评论弹窗与发布弹窗
@Builder
commentModal() {
Column() {
Column() {
Row() {
Text('⬇')
.fontSize(18)
.fontColor('#6B5B8A')
.padding({ left: 6, right: 6, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.14)')
.onClick(() => {
this.showComment = false;
})
Row() {
}
.layoutWeight(1)
Text('评论区 ' + getCommentCount() + '条')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
Row() {
}
.layoutWeight(1)
Text('按热度')
.fontSize(11)
.fontColor('#A78BFA')
}
.width('100%')
.margin({ bottom: 10 })
评论弹窗commentModal的布局从上到下分为四个区域:头部导航条、热评提示条、评论列表和底部输入栏。头部导航条是一个Row,横向排列了关闭按钮、标题和排序方式。关闭按钮使用"⬇"Emoji表示向下收起的语义,点击设置this.showComment = false关闭弹窗。
标题"评论区 8条"居中显示,两侧各有一个空Row占位layoutWeight(1),实现了标题居中的效果——这种"左占位+内容+右占位"的居中模式是ArkTS中实现水平居中的常用技巧,当没有justifyContent(FlexAlign.Center)时,通过等权重的空白占位可以实现相同效果。
Row({ space: 8 }) {
Text('🔥')
.fontSize(14)
Text('热评抢先看,登录后可回复')
.fontSize(11)
.fontColor('#9D8FC2')
}
.width('100%')
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(10)
.backgroundColor('rgba(34,211,238,0.1)')
.margin({ bottom: 10 })
热评提示条是一个带青色半透明背景的Row,显示火苗Emoji和提示文本。这个提示条的作用是引导用户关注热评内容,同时提示登录后才能回复的功能限制。
Scroll() {
Column() {
ForEach(getCommentList(), (c: Comment) => {
Row({ space: 10 }) {
Column() {
Text('👤')
.fontSize(18)
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor('rgba(139,92,246,0.2)')
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(c.user)
.fontSize(12)
.fontColor('#C4B5E0')
.fontWeight(FontWeight.Medium)
Text(c.level)
.fontSize(9)
.fontColor('#22D3EE')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(8)
.backgroundColor('rgba(34,211,238,0.14)')
}
Text(c.content)
.fontSize(12)
.fontColor('#D8CCEF')
.lineHeight(18)
Row({ space: 10 }) {
Text(c.time)
.fontSize(10)
.fontColor('#6B5B8A')
Text('💬 ' + c.replies)
.fontSize(10)
.fontColor('#6B5B8A')
Text('👍 ' + c.likes)
.fontSize(10)
.fontColor('#F472B6')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.06)')
.margin({ bottom: 8 })
}, (c: Comment) => c.id + '')
}
.width('100%')
}
.constraintSize({ maxHeight: '46%' })
.scrollBar(BarState.Off)
评论列表区域使用Scroll容器嵌套Column和ForEach渲染所有评论。ForEach接收三个参数:数据数组getCommentList()、子组件生成函数(c: Comment) => { ... }和键值生成函数(c: Comment) => c.id + ''。键值生成函数将评论的id字段(数字类型)转换为字符串作为唯一标识,框架通过这个键值在数据变化时进行Diff对比。
每条评论卡片是一个Row容器,左侧是34px圆形头像占位(显示"👤"Emoji),右侧是评论内容区。内容区包含三行:用户名+等级标签、评论正文、时间+回复数+点赞数。等级标签(如"Lv8")使用青色背景的小标签,与用户名并排显示。
Scroll容器设置了constraintSize({ maxHeight: '46%' }),限制评论列表的最大高度为屏幕的46%,超出部分通过滚动查看。scrollBar(BarState.Off)隐藏了滚动条,使界面更加简洁。BarState枚举有三个值:Off(隐藏)、On(显示)、Auto(自动显示/隐藏)。
Row({ space: 10 }) {
Row() {
Text('说点什么…')
.fontSize(12)
.fontColor('#6B5B8A')
}
.layoutWeight(1)
.height(38)
.borderRadius(19)
.backgroundColor('rgba(139,92,246,0.12)')
.justifyContent(FlexAlign.Center)
Button() {
Text('发布')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.height(38)
.padding({ left: 18, right: 18 })
.borderRadius(19)
.backgroundColor('#22D3EE')
.onClick(() => {
this.showComment = false;
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(16)
.borderRadius({ topLeft: 20, topRight: 20 })
.backgroundColor('#1E1136')
.translate({ y: 18 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
.transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}
底部输入栏是一个Row,包含一个占位的输入框(显示"说点什么…"灰色提示文本)和一个青色的"发布"按钮。整个评论弹窗的外层Column设置了justifyContent(FlexAlign.End),使弹窗内容靠底部对齐——这是底部弹窗的常见设计,弹窗从底部弹出,占据屏幕下方区域。translate({ y: 18 })使弹窗内容向下偏移18px,产生一种微微下沉的视觉效果。
@Builder
publishModal() {
Column() {
Column() {
Text('✍️ 发布星动态')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ bottom: 12 })
Row() {
Text('此刻的星际见闻、观测笔记或脑洞…')
.fontSize(12)
.fontColor('#6B5B8A')
}
.width('100%')
.height(90)
.borderRadius(14)
.backgroundColor('rgba(139,92,246,0.1)')
.padding(12)
发布弹窗publishModal用于发布动态内容。弹窗标题"✍️ 发布星动态"使用16px粗体字。下方是一个90px高度的多行文本输入占位区,显示灰色提示文本"此刻的星际见闻、观测笔记或脑洞…"。这是一个Row容器模拟的文本输入框外观——在实际应用中应替换为TextArea组件。
Row({ space: 8 }) {
Text('📷 图片')
.fontSize(11)
.fontColor('#A78BFA')
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.14)')
Text('🎥 视频')
.fontSize(11)
.fontColor('#22D3EE')
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('rgba(34,211,238,0.12)')
Text('📡 直播')
.fontSize(11)
.fontColor('#FBBF24')
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('rgba(251,191,36,0.12)')
}
.margin({ top: 12 })
附件选择区是一个Row,包含三个pill样式的标签:图片(紫色)、视频(青色)、直播(黄色)。每个标签使用对应主题色的文本颜色和半透明背景,色彩编码帮助用户快速识别不同附件类型。这种"颜色编码"的视觉设计在这一应用中贯穿始终——不同类型的内容使用固定的颜色主题,形成了视觉一致性。
Text('选择话题')
.fontSize(12)
.fontColor('#9D8FC2')
.margin({ top: 14, bottom: 8 })
Row({ space: 8 }) {
Text('#织女星系脉冲#')
.fontSize(10)
.fontColor('#F472B6')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('rgba(244,114,182,0.14)')
Text('#太阳风暴#')
.fontSize(10)
.fontColor('#FBBF24')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('rgba(251,191,36,0.14)')
Text('#火星农垦#')
.fontSize(10)
.fontColor('#34D399')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('rgba(52,211,153,0.14)')
Text('#太空电梯#')
.fontSize(10)
.fontColor('#22D3EE')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('rgba(34,211,238,0.14)')
}
话题选择区展示了四个热门话题标签,每个标签使用话题对应的主题色。这些话题标签可以与发布内容关联,增加内容的可发现性。标签的视觉样式与附件选择区的pill设计一致,保持了视觉统一性。
Row({ space: 10 }) {
Button() {
Text('存草稿')
.fontSize(13)
.fontColor('#9D8FC2')
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(139,92,246,0.14)')
.onClick(() => {
this.showPublish = false;
})
Button() {
Text('🚀 发射动态')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.linearGradient({
angle: 90,
colors: [['#A78BFA', 0.0], ['#22D3EE', 1.0]]
})
.onClick(() => {
this.showPublish = false;
})
}
.width('100%')
.margin({ top: 16 })
底部操作区有两个按钮:“存草稿"和"发射动态”。"存草稿"按钮使用半透明紫色背景,"发射动态"使用从紫到青的渐变背景。两个按钮都设置this.showPublish = false关闭弹窗——在Demo中两个操作的效果相同,但在实际应用中会执行不同的业务逻辑(保存草稿 vs 发布到服务器)。“发射动态"的文案设计巧妙地结合了星际主题,将"发布"操作包装为"发射”,增强了应用的沉浸感。
十、话题与视频弹窗
@Builder
topicModal() {
Column() {
Column() {
Row({ space: 10 }) {
Column() {
Text(this.selTopic.rank + '')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#0D0718')
}
.width(46)
.height(46)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [['#FBBF24', 0.0], ['#F472B6', 1.0]]
})
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row({ space: 6 }) {
Text('# ' + this.selTopic.title + ' #')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
if (this.selTopic.tag !== '') {
Text(this.selTopic.tag)
.fontSize(9)
.fontColor('#F87171')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(8)
.backgroundColor('rgba(248,113,113,0.2)')
}
}
Row({ space: 8 }) {
Text('热度 ' + this.selTopic.heat)
.fontSize(11)
.fontColor('#FBBF24')
Text(this.selTopic.trend)
.fontSize(11)
.fontColor('#34D399')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
话题弹窗topicModal的头部区域是一个Row,左侧是46px的排名徽章(使用从黄到粉的渐变背景,深色排名数字),右侧是话题标题和热度信息。排名徽章使用this.selTopic.rank + ''将数字转换为字符串。
话题标题格式化为"# 标题 #"的样式,右侧的条件渲染if (this.selTopic.tag !== '')检查热度标签是否为空——当标签不为空时显示红色小标签(“爆"或"热”)。这种条件渲染是ArkTS的if指令在UI构建中的直接应用:当条件为true时渲染对应组件,为false时跳过。
Row() {
Row({ space: 8 }) {
Text('👥')
.fontSize(12)
Text('128.6万人正在讨论')
.fontSize(11)
.fontColor('#9D8FC2')
}
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.12)')
Row() {
}
.layoutWeight(1)
Text('热度走势 ▁▃▅▇')
.fontSize(11)
.fontColor('#A78BFA')
}
.width('100%')
.margin({ top: 12 })
讨论人数和热度走势区域使用了一个巧妙的Unicode字符模拟的迷你图表——"▁▃▅▇"这四个字符利用Unicode方块字符的不同高度,模拟了一个上升趋势的柱状图。这是一种零成本的文本可视化方案,无需绘制Canvas或引入图表库,仅靠文本字符就传达了"热度上升"的视觉信息。
Text('话题观点榜')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 14, bottom: 8 })
Column() {
Row({ space: 10 }) {
Column() {
Text('🔭')
.fontSize(16)
}
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor('rgba(139,92,246,0.18)')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('信号源尚未确认,谨慎解读')
.fontSize(12)
.fontColor('#D8CCEF')
Text('轨道漫游者 · 8.2万赞')
.fontSize(10)
.fontColor('#6B5B8A')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(10)
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.08)')
话题观点榜展示了两条观点卡片,每条包含一个圆形Emoji图标、观点文本和点赞信息。这些观点内容是静态硬编码的,展示了话题下的高质量用户观点。
Row({ space: 10 }) {
Button() {
Text('💬 参与讨论')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('#F472B6')
.onClick(() => {
this.showComment = true;
})
Button() {
Text('+ 关注话题')
.fontSize(13)
.fontColor('#A78BFA')
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(139,92,246,0.16)')
.onClick(() => {
this.showFollow = true;
})
}
.width('100%')
.margin({ top: 14 })
话题弹窗底部有两个按钮:"参与讨论"打开评论弹窗(this.showComment = true),"关注话题"打开关注成功弹窗(this.showFollow = true)。这里体现了弹窗的链式调用模式——从一个弹窗的按钮触发另一个弹窗的打开,形成了多层次的交互流程。
@Builder
videoModal() {
Column() {
Column() {
Column() {
Text('▶')
.fontSize(44)
.fontColor('#FFFFFF')
}
.width('100%')
.height(180)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [['#4C2E8C', 0.0], ['#22D3EE', 1.0]]
})
.justifyContent(FlexAlign.Center)
视频弹窗videoModal的顶部是180px高度的视频播放区域占位,使用从深紫到青色的渐变背景,居中显示白色"▶"播放图标。这个占位区域模拟了视频播放器的外观,44px字号的播放按钮图标在视觉上占据中心位置。
Text(this.selVideo.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 12 })
Row({ space: 8 }) {
Text(this.selVideo.tag)
.fontSize(10)
.fontColor('#22D3EE')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('rgba(34,211,238,0.14)')
Text('UP主 ' + this.selVideo.up)
.fontSize(11)
.fontColor('#9D8FC2')
Text('⏱ ' + this.selVideo.duration)
.fontSize(11)
.fontColor('#9D8FC2')
}
.margin({ top: 8 })
视频标题和信息行展示了视频标题、类型标签、UP主名称和时长。类型标签使用青色pill样式,与视频的主题色一致。时长前缀"⏱"是一个时钟Emoji,增强了时间信息的视觉识别度。
Row() {
Column() {
Text(this.selVideo.plays)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#60A5FA')
Text('播放量')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text(this.selVideo.likes)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F472B6')
Text('点赞')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('4K/60帧')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FBBF24')
Text('画质')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.1)')
.margin({ top: 12 })
视频统计数据区域展示了播放量(蓝色)、点赞(粉色)和画质(黄色)三个指标。其中"4K/60帧"是硬编码的文本,表示视频的画质规格。三色编码与文章详情弹窗的统计数据区保持了一致的颜色语义——蓝色用于播放/阅读量,粉色用于互动量,黄色用于特殊指标。
Row() {
Text('弹幕 3642 条')
.fontSize(11)
.fontColor('#9D8FC2')
Row() {
}
.layoutWeight(1)
Text('线路一 · 蓝光')
.fontSize(11)
.fontColor('#34D399')
}
.width('100%')
.margin({ top: 10 })
Row({ space: 10 }) {
Button() {
Text('👍 点赞')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('#22D3EE')
Button() {
Text('💬 评论')
.fontSize(13)
.fontColor('#A78BFA')
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(139,92,246,0.16)')
.onClick(() => {
this.showComment = true;
})
}
.width('100%')
.margin({ top: 14 })
弹幕信息和播放线路行使用"左内容+右内容"的布局,通过空Row().layoutWeight(1)实现左右分离。底部操作区的"点赞"和"评论"按钮延续了其他弹窗的双按钮设计模式。"评论"按钮触发this.showComment = true,再次体现了弹窗的链式调用。
十一、频道、搜索与分享弹窗
@Builder
channelModal() {
Column() {
Column() {
Row({ space: 12 }) {
Column() {
Text('📡')
.fontSize(24)
}
.width(52)
.height(52)
.borderRadius(16)
.backgroundColor(this.selChannel.color)
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text(this.selChannel.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
Text(this.selChannel.desc)
.fontSize(11)
.fontColor('#9D8FC2')
Text('订阅 ' + this.selChannel.subs)
.fontSize(11)
.fontColor(this.selChannel.color)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
频道弹窗channelModal的头部区域展示了频道图标、名称、描述和订阅人数。注意图标容器的backgroundColor使用了this.selChannel.color——这是频道数据中携带的颜色值,每个频道有自己的主题色。订阅人数的fontColor也使用了相同的颜色值,实现了频道主题色的视觉一致性。这种数据驱动的颜色设计让每个频道弹窗在视觉上都有独特性。
Row() {
Column() {
Text(this.selChannel.hot + '')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FBBF24')
Text('热度指数')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('1242')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#60A5FA')
Text('累计发文')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('98%')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#34D399')
Text('好评率')
.fontSize(10)
.fontColor('#9D8FC2')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.1)')
.margin({ top: 14 })
频道统计区展示了热度指数、累计发文和好评率三个指标。其中热度指数来自数据this.selChannel.hot,而累计发文"1242"和好评率"98%"是硬编码的静态值。在实际应用中,这些数据应从API获取。
Text('频道热文')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 14, bottom: 8 })
ForEach(getTopFeed(), (f: NewsItem) => {
Row({ space: 8 }) {
Text(f.cover)
.fontSize(16)
Text(f.title)
.fontSize(12)
.fontColor('#D8CCEF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.padding({ top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor('rgba(139,92,246,0.06)')
.margin({ bottom: 6 })
}, (f: NewsItem) => 'ch' + f.id)
频道热文列表使用ForEach渲染前三条资讯。每条热文是一个Row,包含Emoji封面和标题。标题使用maxLines(1)限制为一行,textOverflow({ overflow: TextOverflow.Ellipsis })设置溢出时显示省略号——这是ArkTS文本溢出处理的标准方式,TextOverflow.Ellipsis枚举值表示以省略号"…"标记被截断的文本。
ForEach的键值生成函数(f: NewsItem) => 'ch' + f.id使用了前缀"ch"加上id值,这种前缀策略是为了避免不同ForEach之间的键值冲突——因为同一个NewsItem数据可能在不同弹窗的ForEach中被渲染,使用不同的前缀(如"ch"、“au”、“sb”)确保每个ForEach的键值空间独立。
Row({ space: 10 }) {
Button() {
Text('+ 订阅频道')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor(this.selChannel.color)
.onClick(() => {
this.showFollow = true;
})
Button() {
Text('浏览全部')
.fontSize(13)
.fontColor('#A78BFA')
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(139,92,246,0.16)')
.onClick(() => {
this.showChannel = false;
})
}
.width('100%')
.margin({ top: 10 })
}
频道弹窗底部按钮:"订阅频道"使用频道主题色作为背景色(this.selChannel.color),"浏览全部"关闭弹窗。订阅按钮触发关注成功弹窗,再次体现了弹窗链式调用模式。
@Builder
searchModal() {
Column() {
Column() {
Row({ space: 10 }) {
Row() {
Text('🔍 搜索星闻·作者·频道')
.fontSize(12)
.fontColor('#6B5B8A')
}
.layoutWeight(1)
.height(36)
.borderRadius(18)
.backgroundColor('rgba(139,92,246,0.12)')
.justifyContent(FlexAlign.Center)
Text('取消')
.fontSize(13)
.fontColor('#A78BFA')
.onClick(() => {
this.showSearch = false;
})
}
.width('100%')
搜索弹窗searchModal的顶部是一个搜索输入占位和"取消"按钮。输入占位使用justifyContent(FlexAlign.Center)使提示文本居中。"取消"按钮点击关闭弹窗。
Text('🚀 星际热搜词')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 14, bottom: 8 })
Row({ space: 8 }) {
Text('#织女星系脉冲#')
.fontSize(11)
.fontColor('#F472B6')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(244,114,182,0.14)')
Text('#太阳风暴#')
.fontSize(11)
.fontColor('#FBBF24')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(251,191,36,0.14)')
Text('#晨昏线号#')
.fontSize(11)
.fontColor('#22D3EE')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(34,211,238,0.12)')
Text('#大红斑#')
.fontSize(11)
.fontColor('#34D399')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(52,211,153,0.12)')
}
.margin({ top: 4 })
热搜词区域展示了四个热门搜索词,使用不同颜色的pill标签。这些搜索词与话题数据中的标题相关联,用户点击后可以快速搜索对应话题的内容。
Text('🕐 搜索历史')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 16, bottom: 8 })
Column() {
Row() {
Text('引力波 脉冲')
.fontSize(12)
.fontColor('#9D8FC2')
Row() {
}
.layoutWeight(1)
Text('🕒 12:42')
.fontSize(10)
.fontColor('#6B5B8A')
}
.padding({ top: 9, bottom: 9 })
// ... 更多历史记录
}
.width('100%')
搜索历史区域是一个Column,包含四条历史记录。每条记录是一个Row,左侧是搜索关键词,右侧是搜索时间。使用"左内容+空占位+右内容"的模式实现两端对齐。这些历史记录是硬编码的静态数据,在实际应用中应从本地存储读取。
Row({ space: 10 }) {
Button() {
Text('🧹 清空历史')
.fontSize(13)
.fontColor('#9D8FC2')
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(139,92,246,0.14)')
.onClick(() => {
this.showSearch = false;
})
Button() {
Text('查看热搜榜')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('#F472B6')
.onClick(() => {
this.showSearch = false;
this.selTopic = HOT_TOPICS[0];
this.showTopic = true;
})
}
.width('100%')
.margin({ top: 14 })
搜索弹窗底部有两个按钮:"清空历史"关闭弹窗(Demo中简化处理),"查看热搜榜"先关闭搜索弹窗(this.showSearch = false),再打开话题弹窗(this.showTopic = true)。这里展示了先关闭当前弹窗再打开新弹窗的顺序操作——两个状态赋值语句在同一个事件处理函数中执行,框架会在下一帧统一处理状态变化和UI更新。
@Builder
shareModal() {
Column() {
Column() {
Text('分享到')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ bottom: 14 })
Row({ space: 18 }) {
Column({ space: 6 }) {
Column() {
Text('💬')
.fontSize(20)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor('rgba(52,211,153,0.2)')
.justifyContent(FlexAlign.Center)
Text('星友圈')
.fontSize(10)
.fontColor('#9D8FC2')
}
// ... 更多分享渠道
}
.width('100%')
.justifyContent(FlexAlign.Center)
分享弹窗shareModal展示了一个分享渠道网格。每个渠道是一个Column,包含44px圆形图标和标签文本。两个Row各包含四个渠道,通过justifyContent(FlexAlign.Center)使渠道图标在行内居中分布。八个分享渠道包括:星友圈、复制链接、收藏、剪贴板、轨道快讯、打印、生成长图、举报,涵盖了社交分享、实用工具和内容管理三类功能。
十二、作者与关注弹窗
@Builder
authorModal() {
Column() {
Column() {
Row({ space: 12 }) {
Column() {
Text('🧑🚀')
.fontSize(26)
}
.width(56)
.height(56)
.borderRadius(28)
.linearGradient({
angle: 135,
colors: [['#8B5CF6', 0.0], ['#22D3EE', 1.0]]
})
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(this.selAuthor.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
Text('Lv' + this.selAuthor.level)
.fontSize(9)
.fontColor('#FBBF24')
.padding({ left: 5, right: 5, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('rgba(251,191,36,0.18)')
}
Text(this.selAuthor.field + '领域优质创作者')
.fontSize(11)
.fontColor('#9D8FC2')
Row({ space: 10 }) {
Text('粉丝 ' + this.selAuthor.fans)
.fontSize(11)
.fontColor('#F472B6')
Text('·')
.fontSize(11)
.fontColor('#6B5B8A')
Text('文章 ' + this.selAuthor.articles + ' 篇')
.fontSize(11)
.fontColor('#60A5FA')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
作者弹窗authorModal的头部区域展示了创作者头像(使用渐变背景的圆形容器)、名称、等级标签、领域描述和粉丝/文章统计。等级标签"Lv9"通过'Lv' + this.selAuthor.level字符串拼接生成,将数字类型转换为带"Lv"前缀的展示文本。这里直接使用+运算符进行字符串拼接,ArkTS/TypeScript会自动将数字转换为字符串。
Column({ space: 6 }) {
Row() {
Text('创作力')
.fontSize(11)
.fontColor('#9D8FC2')
Row() {
}
.layoutWeight(1)
Text(this.selAuthor.level * 10 + '')
.fontSize(11)
.fontColor('#FBBF24')
}
.width('100%')
Row() {
Row() {
}
.width(this.selAuthor.level * 10 + '%')
.height(8)
.borderRadius(4)
.linearGradient({
angle: 90,
colors: [['#8B5CF6', 0.0], ['#FBBF24', 1.0]]
})
.animation({ duration: 320 })
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.1)')
.margin({ top: 12 })
创作力进度条是作者弹窗的一个视觉亮点。进度条的文本数值通过this.selAuthor.level * 10 + ''计算——将等级(如9)乘以10得到百分比数值(90),再拼接为字符串。进度条本身的宽度通过.width(this.selAuthor.level * 10 + '%')设置为百分比字符串,如'90%'。这里level字段的数字类型至关重要——如果是字符串类型则无法参与乘法运算,这也是Creator接口中level字段被设计为number类型的根本原因。
进度条使用了从紫到黄的90度线性渐变,并设置了animation({ duration: 320 })动画效果。当弹窗打开、进度条首次渲染时,320毫秒的动画会让进度条从0增长到目标宽度,产生一种动态填充的视觉效果。这是ArkTS动画系统的简洁应用——通过animation修饰符为属性变化自动添加过渡动画。
Text('近期代表作')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
.margin({ top: 12, bottom: 8 })
ForEach(getTopFeed(), (f: NewsItem) => {
Row({ space: 8 }) {
Text(f.cover)
.fontSize(14)
Column({ space: 2 }) {
Text(f.title)
.fontSize(12)
.fontColor('#D8CCEF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(f.reads + ' 阅读 · ' + f.comments + ' 评论')
.fontSize(10)
.fontColor('#6B5B8A')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.padding(8)
.borderRadius(10)
.backgroundColor('rgba(139,92,246,0.06)')
.margin({ bottom: 6 })
}, (f: NewsItem) => 'au' + f.id)
近期代表作列表使用ForEach渲染前三条资讯,键值前缀为"au"(author的缩写)。每条代表作展示Emoji封面、标题和阅读/评论统计。maxLines(1)配合textOverflow({ overflow: TextOverflow.Ellipsis })确保标题在一行内显示,溢出部分以省略号标记。
Row({ space: 10 }) {
Button() {
Text('+ 关注 (' + this.followNum + ')')
.fontSize(13)
.fontColor('#0D0718')
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('#22D3EE')
.onClick(() => {
this.followNum = this.followNum + 1;
this.showFollow = true;
})
Button() {
Text('💬 私信')
.fontSize(13)
.fontColor('#A78BFA')
}
.layoutWeight(1)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(139,92,246,0.16)')
.onClick(() => {
this.showComment = true;
})
}
.width('100%')
.margin({ top: 10 })
}
作者弹窗底部按钮:"关注"按钮显示当前关注数this.followNum,点击时先执行this.followNum = this.followNum + 1递增关注数,再执行this.showFollow = true打开关注成功弹窗。这是一个状态更新的实际案例——followNum是@State变量,递增后框架会自动更新按钮文本中的数字显示。两个操作(递增关注数和打开弹窗)在同一个事件处理函数中顺序执行,框架会在下一帧统一应用状态变化。
@Builder
followModal() {
Column() {
Column() {
Text('🔔 关注成功')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F5F0FF')
Text('已为你开启更新提醒,有新内容将第一时间推送')
.fontSize(12)
.fontColor('#9D8FC2')
.margin({ top: 8 })
.textAlign(TextAlign.Center)
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('📮')
.fontSize(22)
Text('仅推送重要')
.fontSize(10)
.fontColor('#9D8FC2')
}
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor('rgba(139,92,246,0.1)')
Column({ space: 4 }) {
Text('📬')
.fontSize(22)
Text('全部推送')
.fontSize(10)
.fontColor('#22D3EE')
}
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor('rgba(34,211,238,0.14)')
Text(f.hot)
.fontSize(8)
更多推荐




所有评论(0)