鸿蒙 ArkTS 实战:星座查询 Horoscope

📅 2026/8/7 11:52:33
鸿蒙 ArkTS 实战:星座查询 Horoscope
引言星座查询是社交、生活、娱乐类应用里十分常见的轻量工具用户想知道自己属于哪个星座、每个星座的日期区间是什么、各自的性格特点是怎样的。本文要讲解的示例 28「星座查询 Horoscope」用 ArkTS 在单个页面里实现了「今日星座提示 十二星座宫格 选中星座详情」的完整交互这个示例最大的亮点是它没有引入任何第三方算法库而是自己用一段十几行的数学逻辑完成「根据月日判断星座」的核心功能通过一个代表各月星座起始日的数组加上一次取模运算就能把任意日期精确映射到 12 个星座之一。配合Grid网格布局、State状态管理与生命周期回调页面能够在打开的一瞬间自动定位「今日星座」并高亮显示。全文同样分为八个章节从整体功能、核心知识点、源码逐段解析到星座区间判定算法的推导、运行效果、可扩展方向与调试技巧所有内容都严格对应源码中的真实变量名与函数名希望帮助你把「数据、算法、界面」三者的协作关系一次看懂。一、应用概述与功能「星座查询」页面自上而下由五个部分组成功能清晰、层次分明。第一是顶部返回栏。左上角是蓝色「返回」按钮点击后调用router.back()回到上一页中间是加粗标题「星座查询」右侧用Blank()弹性占位保证布局两端对齐。第二是今日星座提示行。它显示两段信息左侧灰字「今日 x月x日」右侧橙色加粗「所属白羊座」之类的文案。这行数据并非写死的而是在页面出现时通过new Date()读取系统当前日期再调用getZodiac算法计算出来的因此每一天打开应用这里都会自动跟着变化。第三是十二星座宫格。页面用Grid以四列布局排列 12 个星座单元格每个单元格上半部分显示星座名下半部分显示日期区间如「3.21-4.19」。单元格有三级背景态默认白色「今日星座」所在格为浅黄色#fff7e6用户当前选中的格为主蓝色#1a6cff并带 2 号蓝色描边。三者叠加时选中态优先于今日态。第四是选中星座详情卡。页面底部有一张白色圆角卡展示当前选中星座的完整信息星座名24 号加粗、如果恰好是今日星座则出现一个橙色「今日星座」小徽标、日期区间、幸运色主题蓝加粗以及一条 24 行高的人格描述。整张卡片用Divider分割线把「基本信息」与「性格描述」分成上下两段。第五是整体滚动容器。全部内容包在一个Scroll里配scrollBar(BarState.Off)隐藏滚动条页面背景为浅灰#f2f3f5白色宫格与详情卡浮于其上视觉风格与示例 27 的天气卡片保持统一。整页的交互只有「点击宫格切换选中星座」一处但背后同时涉及日期算法、网格布局、条件渲染与动态样式四条技术线信息量并不小。二、核心知识点示例 28 覆盖的知识点可以分为五类逐一拆解如下。第一是State状态管理。页面声明了四个状态stars保存 12 个星座的完整数据也是整个页面的数据源、selected记录当前选中星座的下标、current记录今日所属星座的下标、todayText保存格式化后的今日日期文本。其中selected与current都是 number 类型界面上的高亮与详情展示全部由这两个下标驱动。第二是interface数据模型。单个星座包含名称、日期区间、幸运色、性格描述四个字段源码用interface Star { name: string; date: string; lucky: string; desc: string; }建模再用State stars: Star[]装载 12 条预置数据。把数据集中放在一个数组里后续无论渲染宫格还是详情卡都只依赖这一个数据源不会出现多份数据不同步的问题。第三是网格布局Grid与GridItem。宫格部分使用Grid()容器通过.columnsTemplate(1fr 1fr 1fr 1fr)把宽度切成四等份配合columnsGap(10)与rowsGap(10)设置行列间距height(260)固定整体高度12 个单元格恰好组成三行四列。每个单元格必须用GridItem包裹这是网格布局的基本要求。第四是日期区间判定算法。核心函数是getZodiac(month, day)它先用一个borders数组存放「每个月星座更替的起始日」再通过day borders[idx]判断日期是否落在前一个星座区间内若是则用(idx 11) % 12做带环绕的索引回退。这一段是理解全篇的关键第四章会专门推导。第五是生命周期与条件渲染。aboutToAppear()在页面首次出现时读取系统时间、计算今日星座并把selected对齐到current让页面一打开就自动定位aboutToDisappear()负责清理预留的定时器。详情卡里的「今日星座」橙色徽标则是用if (this.selected this.current) { ... }条件渲染出来的只有选中项恰好是今日星座时才出现。此外ForEach的 key 生成器、cellBg等三个封装单元格样式的工具函数、router.back()路由返回也都是值得留意的实现细节。最后再强调一点本页面的 UI 只依赖stars、selected、current、todayText四个状态其中三个是展示用的标量一个是数据源数组。状态数量的克制意味着每次交互需要更新的点很少框架的重绘开销也就很小。「小状态集 大数组数据」这一经典组合正是这类查询型页面能保持流畅的关键。以上便是示例 28 涉及的全部核心知识点下一章进入源码逐段解析。三、源码逐段解析源码的组织顺序是「数据模型 → 状态 → 算法 → 生命周期 → 样式工具 → build 布局」我们按顺序拆解。3.1 数据模型与预置数据Star接口用四个字符串字段描述一个星座定义非常简洁interface Star { name: string; date: string; lucky: string; desc: string; }数据源是一条 12 项的数组State stars: Star[]前两条示例为白羊座3.21-4.19幸运色红色性格「热情直率行动力强敢于挑战喜欢新鲜事物」与金牛座4.20-5.20幸运色绿色性格「踏实稳重重视物质享受忠诚可靠耐心十足」。后续十条依次是双子、巨蟹、狮子、处女、天秤、天蝎、射手、摩羯、水瓶、双鱼各自的日期区间、幸运色与性格描述都与星座常识保持一致例如狮子座 7.23-8.22 幸运色金色、天蝎座 10.24-11.22 幸运色紫色、摩羯座 12.22-1.19 幸运色棕色。这样一份完整的 12 星座数据集宫格与详情卡都在复用它做到了「一份数据、多处渲染」。注意date字段的格式统一为「月.日-月.日」这正是第四章算法推导时会用到的区间信息。3.2 星座判定算法getZodiac是页面最核心的函数输入月份与日期返回星座在stars数组中的下标getZodiac(month: number, day: number): number { const borders: number[] [20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22]; let idx: number month - 1; if (day borders[idx]) { idx (idx 11) % 12; } return idx; }borders数组的第 i 个元素代表「第 i 个月里该月对应星座的起始日」。例如borders[0] 20表示 1 月的星座水瓶座从 1 月 20 日开始borders[11] 22表示 12 月的摩羯座从 12 月 22 日开始。算法先把下标定位到当月星座再判断日期是否早于其起始日早于则退回上一个月对应的星座并用(idx 11) % 12解决 1 月回退到 12 月的环形问题。3.3 生命周期初始化页面出现时读取系统时间并完成三件事拼出「x月x日」文本、算出今日星座下标、让选中项默认等于今日星座aboutToAppear(): void { const now: Date new Date(); const month: number now.getMonth() 1; const day: number now.getDate(); this.todayText month 月 day 日; this.current this.getZodiac(month, day); this.selected this.current; }注意now.getMonth()返回的月份从 0 开始1 月是 0所以这里必须加 1 才是真实的月份数getDate()则直接返回当月的第几天无需换算。this.selected this.current一行让页面一打开就高亮今日星座用户体验非常顺手。3.4 单元格样式工具函数三个小函数把「选中 / 今日 / 默认」三态的样式决策集中起来UI 层调用时完全不用关心判断逻辑这里展示其中两个核心函数cellBg(idx: number): string { if (idx this.selected) return #1a6cff; if (idx this.current) return #fff7e6; return #ffffff; } cellTextColor(idx: number): string { if (idx this.selected) return #ffffff; return #333333; }cellBg的优先级是「选中 今日 默认」先判断selected再判断currentcellTextColor让选中格的文字变白以适配蓝色底。第三个函数cellDateColor与cellTextColor结构完全同构负责返回日期文字的浅蓝#d9e6ff选中态或灰#9a9a9a默认态。把样式逻辑从build()里抽成带返回值的小函数是保持声明式代码整洁的常见手法也让三个状态的配色规则可以集中调整。3.5 星座宫格宫格由Grid容器承载四列等宽每个单元格是一个GridItemGrid() { ForEach(this.stars, (item: Star, idx: number) { GridItem() { Column({ space: 2 }) { Text(item.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor(this.cellTextColor(idx)) Text(item.date).fontSize(10).fontColor(this.cellDateColor(idx)) } .borderRadius(8) .borderWidth(idx this.selected ? 2 : 0).borderColor(#1a6cff) .backgroundColor(this.cellBg(idx)) .onClick(() { this.selected idx; }) } }, (item: Star, idx: number) idx.toString()) } .columnsTemplate(1fr 1fr 1fr 1fr).columnsGap(10).rowsGap(10).height(260)单元格内容是一个两行结构上面是星座名下面是日期区间字号分别 14 与 10主次分明内容通过justifyContent(FlexAlign.Center)垂直居中。borderWidth(idx this.selected ? 2 : 0)让选中格出现 2vp 的蓝色描边点击单元格则执行this.selected idx更新状态详情卡随之刷新。四、关键实现细节分析4.1 星座日期区间判定算法的完整推导borders数组实际上把 12 个星座的「换座日」压缩成了一行数据这是整个算法最精妙的地方。数组下标 0 到 11 依次对应 1 月到 12 月每个元素代表该月内星座更替的日期。以双子座为例它的区间是 5.21-6.21意味着 5 月 21 日起进入双子座因此borders[4] 21而 5 月 20 日之前属于金牛座所以borders[3] 20。算法执行时先令idx month - 1把「当月对应的星座」作为初始候选随后比较day与borders[idx]如果day borders[idx]说明今天还没到换座日实际应属于前一个星座于是idx回退一位。回退采用(idx 11) % 12而不是简单的idx - 1原因在于 1 月时idx 0减一变成负数会导致数组越界取模后(0 11) % 12 11恰好指向 12 月的摩羯座环形回退一次完成。我们验算几个边界例子4 月 19 日idx 3borders[3] 2019 小于 20回退到 2即白羊座3.21-4.19正确4 月 20 日20 不小于 20保持下标 3即金牛座正确1 月 10 日idx 010 小于 20回退到 11即摩羯座12.22-1.19正确1 月 20 日20 不小于 20保持 0即水瓶座正确。四个边界全部命中可见这个算法对每个月的交界日都做了精确处理。实际产品中还要考虑「闰年」问题吗答案是不需要。星座的划分只看月与日与年份、是否闰年、2 月有多少天都无关因此getZodiac的两个入参只取month与day天然对任何年份成立。另外这个算法也不受时区影响因为new Date()拿到的是设备本地时间设备显示的是什么日期就按什么日期计算用户身处任何时区结果都是自洽的。4.2 下标驱动的三态视觉selected与current两个下标把「用户行为」与「算法结果」统一到了一套状态体系里点击宫格只改selected打开页面时则把两者对齐。单元格的背景、文字色、描边全部由这两个下标经三目运算或工具函数决定界面与状态严格一一对应。值得注意优先级设计如果今日星座恰好被用户点击选中单元格应显示选中蓝而不是浅黄因此cellBg里先判断selected再判断current两个条件顺序不能颠倒。描边的实现也值得一提borderWidth(idx this.selected ? 2 : 0)在未选中时宽度为 0相当于完全隐藏描边选中时瞬间变为 2vp 的主题蓝描边配合背景色由白转蓝形成「描边 底色」的双重强调。而cellDateColor把选中格内的日期文字调成浅蓝#d9e6ff是为了在蓝色底上依然保持日期可读——若沿用默认灰#9a9a9a灰字压蓝底会显得浑浊。三个颜色函数的分工恰好构成了一套完整的「状态配色矩阵」。4.3 Grid 四列模板与固定高度.columnsTemplate(1fr 1fr 1fr 1fr)表示四列各占一份比例等价于四等分columnsGap(10)与rowsGap(10)定义行列间距为 10vp。12 个元素在四列模板下自动排成三行四列。Grid必须有确定高度才能参与布局源码给出height(260)平均每行约 80vp单元格内部再通过justifyContent(FlexAlign.Center)垂直居中保证名称与日期紧凑地待在格子中央。四列模板还有一个隐藏好处1fr的比例写法让列宽完全由容器宽度决定无论屏幕宽窄四列都自动等分、自动填满不需要关心具体像素。单元格顺序由ForEach按下标依次填充第 1 到第 12 个星座从左上向右下自然排布正好三行。若以后把星座扩充到 13 个Grid会自动补出第四行无需任何代码改动这正是声明式布局「由数据驱动行列」的体现。4.4 条件渲染「今日星座」徽标详情卡顶部的橙色小徽标不是始终存在的而是由if (this.selected this.current)条件渲染控制只有当选中项就是今日星座时才显示「今日星座」四个字其余情况下该区域不占位、不显示。这是 ArkTS 声明式语法if/else在 UI 上的直接用法配合白色圆角背景与#ff8c00橙色形成一处亮眼的点睛。徽标本身是一个Text组件字号 12、白色文字、橙色背景、圆角 4vp四周有padding({ left: 6, right: 6, top: 2, bottom: 2 })视觉上呈现为胶囊形的小标签与宫格里的浅黄高亮遥相呼应让「今日星座」的身份在页面两处同时得到提示。4.5 数据模型与下标安全详情卡用this.stars[this.selected]直接按下标取值之所以敢这么写是因为selected只可能来自两个途径初始时等于current由getZodiac保证落在 0 到 11 之间或点击宫格时由ForEach的下标参数赋值。两个源头都严格收敛在数组范围内因此不需要额外的越界保护。这也从侧面说明状态的设计若能保持「取值范围天然合法」业务代码就可以少写很多防御性的判断。顺带可以做一次安全性的推演stars数组的下标与borders数组的下标遵循同一套排序0 对应白羊、11 对应双鱼二者由getZodiac返回的下标直接对接中间没有任何转换层。因此只要borders的顺序与stars的顺序保持一致算法结果就一定落在正确的星座上。这也提醒我们当项目里同时存在多个按索引对齐的数组或对象时务必让它们的顺序约定统一否则一处顺序错位会引发全盘错乱。五、运行效果与操作指南5.1 如何运行用 DevEco Studio 打开工程完成同步后选择一个模拟器或真机作为运行目标点击 Run 编译安装。应用启动后从入口列表点进「星座查询」这一项就会进入index28.ets渲染出的页面。同样可以使用 Previewer 直接预览打开源码文件点击右侧 Preview 标签即可看到宫格与详情卡的实时渲染点击宫格单元格也能触发选中态变化。5.2 页面打开时的自动定位页面一出现aboutToAppear就会把系统当前日期换算成「x月x日」显示在提示行同时计算出今日星座并把selected对齐到current。也就是说无论你今天哪天打开应用提示行右侧都会显示正确的「所属xxx」宫格中对应星座自动呈现浅黄色详情卡自动展示该星座的信息并带一枚「今日星座」橙色徽标。这个「打开即定位」的行为完全由代码自动完成不需要任何用户操作。顺带可以做一个验证实验把系统时间临时改到 1 月 15 日再打开页面提示行会显示「所属摩羯座」对应格子浅黄高亮改到 1 月 20 日再打开则变为「所属水瓶座」。这直观地证明getZodiac的边界判断20 日换座在真实设备上工作正常也说明aboutToAppear每次进入页面都会重新读取时间无需重启应用就能拿到最新日期。5.3 交互操作与预期效果页面上唯一需要用户操作的是点击宫格。随便点一个星座单元格可以观察到三件事同时发生被点中的格子背景变为主蓝色、文字变白、出现 2 号蓝色描边其余格子恢复默认白色若其中有今日星座则保持浅黄底部的详情卡整体刷新星座名、日期区间、幸运色与性格描述全部切换为新选中项如果新选中项恰好是今日星座卡片顶部会多出「今日星座」徽标否则徽标消失。例如今天是 8 月 15 日打开页面后提示行显示「今日 8月15日 所属狮子座」狮子座格子为浅黄此时点击「天蝎座」狮子座格恢复白色、天蝎座格变蓝详情卡显示天蝎座 10.24-11.22、幸运色紫色及其性格描述若点击回「狮子座」则其格子变蓝且详情卡顶部出现橙色「今日星座」徽标。整个交互流畅、反馈即时充分体现了「改一个selected整页联动刷新」的声明式特征。六、可扩展方向「星座查询」目前是一个漂亮的静态示例把它推向真实产品时可以沿以下方向扩展。第一是接入今日运势。详情卡目前只有固定的性格描述可以增加「今日运势」「爱情指数」「事业指数」等动态字段数据来源可以是本地按日期伪随机生成的规则表也可以是后端接口。这类扩展只需要在Star接口上增加字段、在详情卡中增加展示行即可现有结构无需大改。第二是动态数据源与缓存。stars数组目前写死在代码里可以改成从云端拉取配合本地缓存与更新时间戳星座边界日期、幸运色等数据如果后续调整也只需改数据而不动代码逻辑。第三是日期选择器与手动查询。目前的定位完全依赖系统今天的日期可以加入DatePicker或一个滑动条让用户自由选择「某月某日」实时重算该日所属星座把页面从「今日工具」升级成「任意日期查询」进一步突出getZodiac算法的复用价值。第四是动画与视觉增强。宫格点击时用animateTo给背景色、描边加上过渡动画详情卡切换时让内容做轻微的位移或透明度渐入星座名称还可以配一个圆形图标强化辨识度。第五是组件化拆分。可以把宫格、详情卡分别抽成Component子组件通过Prop接收下标或星座对象页面主体只保留状态与算法的编排。组件拆好后宫格还能复用到「好友星座匹配」「今日运势」等其他页面。第六是分享与营销玩法。选中星座后生成一张带幸运色底色的分享图调用系统分享能力发给好友让工具型页面具备传播属性。第七是国际化与本地化。12 个星座的名称与描述目前是中文硬编码若要出海可以把stars数据抽成按语言区分的资源表只替换展示文案星座区间属于天文常识全球通用getZodiac算法本身可以原样复用改动成本非常低。七、常见问题与调试技巧第一月份取值少算一个月。Date.getMonth()返回 0 到 111 月对应 0如果忘记加 1 直接传入getZodiac会导致所有星座判定整体错位一个月。源码里const month: number now.getMonth() 1;的 1是关键改动时务必保留。第二(idx 11) % 12的意义。如果手误改成idx - 11 月 20 日之前的日期会算出-1用它索引stars数组会越界。用取模实现环形回退是这段算法能正确处理 1 月边界的原因不要简化掉。第三宫格不换行或布局错乱。Grid的行列数由columnsTemplate决定如果不写它单元格会按照默认单列排列12 个星座会纵向铺开。确认columnsTemplate(1fr 1fr 1fr 1fr)与columnsGap、rowsGap三个属性都写在了Grid上。第四GridItem内容没有撑满。单元格内部的Column需要.width(100%)与.height(100%)才能铺满网格单元配合justifyContent(FlexAlign.Center)才能让星座名与日期正居格子中央如果漏掉尺寸属性内容会缩在角落。第五选中态与今日态显示冲突。cellBg里两个 if 的顺序决定了「选中蓝」优先于「今日浅黄」。如果哪天改成先判断current会出现点击今日星座时格子仍是浅黄的诡异现象排查时优先检查这段优先级。第六selected下标越界。虽然源码已经保证了selected永远合法但在扩展时如果引入「滑动删除星座」这类会改变数组长度的功能请同步约束selected的边界否则this.stars[this.selected]会直接取到undefined并导致渲染异常。第七详情卡刷新不动。点击宫格只更新了selected详情卡依赖this.stars[this.selected]因为stars本身是State数组而selected是独立State二者任一变化都会触发重绘。如果以后把selected改成普通成员变量UI 就再也不会刷新注意保持它的State身份。第八调试技巧。可以在aboutToAppear里用console.info打印month、day、current三个值对照 12 个星座的边界日期验证算法是否正确用 Previewer 快速点击宫格观察三态切换如果徽标不出现检查this.selected this.current的比较是否被改成别的方式。第九关于aboutToAppear与首次渲染。aboutToAppear在组件构建之后、首次渲染之前执行此时给State赋值是安全的页面第一帧就能带上正确的初始数据。如果将来把日期计算逻辑搬进build()每次重绘都会重复执行白白浪费保持「数据准备放生命周期、界面声明放 build」的划分是最稳妥的写法。八、总结示例 28「星座查询」用一个页面把算法、数据与界面三件事编排得干净利落getZodiac用一行borders数组加一次取模运算完成全年的星座区间判定State的selected与current两个下标驱动宫格三态高亮与详情卡联动aboutToAppear让页面打开即定位今日星座Grid的四列模板把 12 个单元格排成工整的三行四列条件渲染则让「今日星座」徽标只在恰当的时候出现。从学习角度看这个示例最有价值的是两处一是那个看似简单的取模算法它把「跨月、跨年」这类容易写错的边界问题压缩成了三个字符理解它等于掌握了一类「环形索引」的编程技巧二是「下标驱动界面」的设计全篇没有一行直接操作 DOM 样式的代码所有的选中、高亮、描边都由数据推导而来这正是声明式 UI 的精髓。建议读者动手做三件事一是把borders数组改成对象映射或枚举体会不同建模方式对代码可读性的影响二是给getZodiac补一个「输入日期区间直接返回星座」的单元测试用一年 365 天的数据跑一遍验证边界正确三是把详情卡抽成子组件并用Prop接收星座数据走一遍组件通信的流程。星座虽小却是把「算法落地为界面」的绝佳范本。