知识付费在线学习实战:HarmonyOS API 24下知了学堂靛蓝学术主题、5Tab差异化布局与音频听书笔记增删改的完整拆解

在信息过载的时代,持续学习已经成为职场人的核心竞争力,而一个好的知识付费App能让碎片化时间变成系统性成长。本文完整拆解一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式框架构建的「知了学堂」知识付费在线教育应用,覆盖靛蓝学术主题配色系统、学习头部连续学习天数三卡、三种靛蓝渐变视觉语言、5Tab差异化布局(首页名师横滑+分类网格+精品课列表/课程大卡列表/听书音频书+章节列表/学习时长柱状图+笔记列表/我的已购+证书)、双动画状态联动、分类六色动态着色、课程价格红字+原价删除线、已购课程动态进度条、音频听书播放控制、笔记增删改三大弹窗、弹窗共享editIdx索引、分类跳转与学习报表跳转等核心技术点,附完整源码与运行效果截图。


一、引言:知识付费时代的碎片化学习

随着移动互联网的深度普及,知识付费已经从"新鲜事物"变成了职场人的日常刚需。然而,课程质量参差不齐、学习路径不清晰、碎片化时间难以利用、学完即忘缺乏沉淀、音频听书体验不佳,正成为知识付费用户的现实痛点。一个优秀的知识付费App,不仅需要覆盖从"发现课程"到"学习沉淀"再到"证书成就"的完整业务闭环,还要在名师推荐、分类导航、精品课程、音频听书、学习数据、笔记管理、会员证书等维度做到精准而高效。

正是在这样的背景下,知了学堂类知识付费在线教育应用应运而生,并迅速成为职场人自我提升的必备工具。一个优秀的知识付费应用,需要以首页为核心入口,横向扩展出课程、听书、学习、我的五大功能模块,每个模块采用完全不同的布局风格——首页=名师横滑+分类网格+精品课列表、课程=大卡课程列表、听书=音频书+章节列表、学习=时长柱状图+笔记列表、我的=已购+证书,形成了一个完整闭环的知识付费学习生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、Stack弹窗叠加、双动画状态联动、靛蓝学术视觉语言、分类六色动态着色、音频听书播放控制、笔记增删改三大弹窗、弹窗共享editIdx索引等关键技术点。

知了学堂应用整体运行效果 - 学习头部连续学习天数三卡与88好书节Banner及名师横滑分类网格精品课列表


二、应用整体架构概览

整个应用采用了"单入口 + 5Tab单排差异化布局 + Stack弹窗叠加"的知识付费应用架构。入口组件 Page819 作为整个应用的根容器,负责管理当前激活的 Tab 状态、双动画状态以及三个弹窗的显隐状态。

应用一共划分为五个一级页面:首页(HOME)、课程(COURSE)、听书(AUDIO)、学习(STUDY)以及我的(MINE)。与之前项目"7Tab两排"不同,本应用回归"5Tab单排"的经典底部导航布局——首页🎓/课程📚/听书🎧/学习📈/我的👤,底部 tabBar 激活态 indigo 靛蓝字加粗,非激活 text3 灰字,通过 ForEach 遍历 TAB_LIST 构建,每个 Tab layoutWeight 1 等分宽度。五个Tab每个布局完全不同——首页采用名师横滑+分类网格+精品课列表、课程采用分类横滑+大卡课程列表、听书采用音频书大卡+章节列表+换一本听、学习采用学习数据三卡+每日时长柱状图+笔记列表+学习成就、我的采用用户卡+已购课程进度条+证书,这种"一页一型"的差异化设计最大程度还原了真实知识付费App的交互体验。

在数据层面,应用定义了一套完整的类型系统,包括色彩主题、名师元数据、课程元数据、音频书元数据、学习笔记元数据、已购课程元数据,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类为载体(MasterItem/CourseItem/AudioBook/StudyNote/MyCourse),配合 5 位名师、8 门课程、4 本音频书、6 条学习笔记、4 门已购课程的模拟数据,模拟了一个真实的知识付费数据环境。所有数据类都带有 constructor 构造函数,使用 new ClassName(...) 方式实例化。应用还定义了 8 个工具函数(catColor分类六色/courseLevelTag课程等级标签/noteIcon笔记图标动态/studentsStr人数格式化/courseRank课程排名/chapterDur章节时长/studyBarH柱状图高度/updateNote更新笔记splice替换对象),将颜色映射、数据计算、增删改逻辑从 UI 层抽离,实现数据与逻辑分离。

在交互层面,应用通过 Stack 布局叠加实现了三类弹窗的显示:新增笔记、编辑笔记、删除笔记。与之前项目"每个弹窗独立 Stack 叠加"一致,本应用三个弹窗各自独立 Stack 叠加,共享 modalOverlay 统一遮罩封装(全屏 mask 遮罩 + 点击关闭)。特别的是,本应用的弹窗数据状态设计十分精细——addModal 新增笔记包含 addTitle/addContent 两个输入状态,editModal 编辑笔记包含 editIdx/editTitle/editContent 状态(editIdx 记录要编辑的笔记索引,editTitle/editContent 预填原笔记标题和内容),delModal 删除笔记复用 editIdx 状态记录要删除的笔记索引,三个弹窗共享 editIdx 状态实现笔记索引的传递,减少了重复状态定义。新增笔记确认后 noteList.unshift 新 StudyNote(Date.now id,标题默认今日一得,内容默认坚持每天记录一点,tag默认职场,time 08-21,likes 0),编辑笔记确认后调用 updateNote(splice替换对象确保响应式刷新,保留原id/tag/time/likes,更新title/content),删除笔记确认后 noteList.splice(editIdx,1) 按索引直接删除,形成了完整的笔记管理增删改闭环。

在视觉层面,本应用独创了"靛蓝学术主题"设计——bg #EEF0FB 靛蓝学术底,indigo #3D4EB5 靛蓝为主色,indigoD #28337D 深靛蓝为深色,gold #C9A227 鎏金为辅助色,title #1E2249 深蓝而非纯黑,与靛蓝学术底形成高对比度的同时带有学术沉稳质感。应用的渐变设计是色彩系统的核心亮点——首页 Banner 使用 linearGradient 135 度从 indigoD #28337D 深靛蓝到 indigo #3D4EB5 靛蓝的两色渐变,我的用户卡使用 linearGradient 135 度从 indigoD #28337D 深靛蓝到 purple #8A5FC0 紫的两色渐变(与首页 Banner 的深靛蓝→靛蓝不同,我的卡用深靛蓝→紫,增加视觉变化),学习页每日学习时长柱状图使用 linearGradient 180 度从 indigo #3D4EB5 靛蓝到 indigoD #28337D 深靛蓝的垂直渐变,三种不同角度和配色的靛蓝渐变贯穿整个应用,形成了统一而丰富的靛蓝渐变视觉语言,是本应用色彩设计的核心特色。学习头部采用连续学习天数三卡(156天靛蓝字chip底/286h鎏金字白底/12本绿字白底)+今日课程横条(今日上新《置身事内》精读版靛蓝字+满200减40鎏金字),是本应用头部的特色设计。应用还独创了"双动画状态联动"设计——aboutToAppear 中 setInterval 650ms 同时切换 breath 和 wave 两个布尔动画状态,breath 用于课程页"加入学习"按钮 opacity 动画(0.75/1,duration 500ms),wave 用于首页 Banner 📚 图标 animation(duration 650ms EaseInOut)、听书页播放按钮 animation(duration 600ms)、笔记 ❤️ 点赞 animation(duration 500ms),两个动画状态各司其职驱动四种不同的动效,形成整体协调的联动动效体系。

知了学堂新增笔记弹窗运行效果 - 弹窗叠加学习页显示笔记标题与内容输入及保存笔记按钮

下面逐页展示应用的五大功能模块运行效果。


三、首页:88好书节Banner、名师横滑与分类网格

首页以"发现学习"为核心,独创"活动Banner+名师横滑+分类网格+精品课列表"的四层复合布局。

首页模块 - 学习头部连续学习天数三卡与88好书节Banner及名师横滑分类网格精品课畅销榜

页面顶部第一部分是 88 好书节活动 Banner,Column 包裹 space 8,padding 16,圆角 18,linearGradient 135 度从 indigoD #28337D 深靛蓝到 indigo #3D4EB5 靛蓝的两色渐变:

  • 活动信息行:Row space 10,活动信息列(layoutWeight 1,左对齐):"88好书节 · 全场 5 折起"白字15号加粗 + "薛兆丰·刘擎·万维钢 大师课同价"白字0.85透明度9号 + 📚图标34号,带 wave animation(duration 650ms EaseInOut),书本图标随 wave 状态微动,营造活动氛围。
  • 参与领券行:Row space 8,"已有 120 万人参加"白字0.85透明度9号 + Column layoutWeight 1 + “领券购买 >”#FFE082鎏金字10号加粗。

第二部分是名师大家横滑,标题"⭐ 名师大家"加粗13号 + Scroll horizontal 遍历 5 位名师,每位名师 Column 包裹 space 6,width 92,padding 10,白底圆角14,border 1px line 色,alignItems Center,包含:

  • 头像:avatar 32号,width 66 height 66 圆角33 dark底 textAlign Center。
  • 姓名:name title深蓝字11号加粗。
  • 头衔:title sub灰字8号 maxLines 1。
  • 关注按钮:已关注时 text3灰字9号加粗 darkL底,未关注时白字9号加粗 indigo底,padding 左右12上下4 圆角10。

5 位名师:薛兆丰(北大经济学教授,6门课,48.2万学生,已关注)、梁宁(产品思维导师,3门课,32.7万,已关注)、刘擎(华东师大哲学教授,4门课,27.5万,未关注)、香帅(金融学者,5门课,21.3万,未关注)、万维钢(科学作家,8门课,39.8万,未关注)。名师横滑让用户快速发现领域大咖,关注按钮两态(已关注灰/未关注靛蓝)清晰区分关注状态,是本应用首页交互设计的亮点。

第三部分是全部分类网格,标题"🗂️ 全部分类"加粗13号 + Flex wrap 遍历 8 个分类(职场/投资/心理/科技/人文/管理/语言/亲子),每个分类 Column 包裹 space 6,width 23%,padding 10,白底圆角14,margin 4,alignItems Center,包含:

  • 分类图标:noteIcon© 24号(职场💼/投资📈/心理🧠/科技🤖/其他📖),width 46 height 46 圆角14 dark底 textAlign Center。
  • 分类名:c title深蓝字10号加粗。
  • 点击事件:onClick(() => { this.tabIdx = 1; }),点击任意分类跳转到课程页(tabIdx=1),实现分类导航跳转。

8 个分类覆盖职场/投资/心理/科技/人文/管理/语言/亲子八大学习领域,分类图标动态映射(noteIcon函数),点击分类跳课程页,是本应用首页导航设计的核心亮点。

第四部分是精品畅销榜,标题行 Row space 8:"🏆 精品畅销榜"加粗13号 layoutWeight 1 + "完整榜单 >"indigo靛蓝字9号 + 遍历 8 门课程,每门课程横向布局,padding 12,白底圆角14,border 1px line 色,alignItems Center,包含:

  • 课程图标:icon 30号,width 60 height 60 圆角16 dark底 textAlign Center。
  • 课程信息列(layoutWeight 1,左对齐,space 5):
    • 标题排名行:Row space 6,课程名(title深蓝字11号加粗 maxLines 1 layoutWeight 1)+ 排名 courseRank(c.id)(NO.X,id<=3时gold鎏金字否则text3灰字,9号加粗)。
    • 讲师信息:c.teacher + " · " + c.lessons + "讲 · " + c.rating + “分”(sub灰字9号)。
    • 标签行:Row space 6,分类标签(catColor(c.cat)动态着色8号,chip底圆角6 padding 左右6上下2)+ 等级标签(level text3灰字8号,dark底圆角6 padding 左右6上下2)。
  • 价格列(右对齐,space 4):现价(red红字13号加粗)+ 原价(text3灰字8号,decoration LineThrough删除线)+ 学过人数(studentsStr(c.students) + “人学过”,text3灰字8号)。

8 门课程覆盖六大分类:薛兆丰的经济学课(投资入门,¥199/¥365,48.2万人,367讲,4.9分,NO.1鎏金)、产品思维30讲(职场进阶,¥99/¥199,32.7万,30讲,4.8分,NO.2鎏金)、西方现代思想40讲(人文入门,¥129/¥259,27.5万,40讲,4.9分,NO.3鎏金)、香帅中国财富报告(投资进阶,¥199/¥399,21.3万,52讲,4.7分,NO.4灰)、精英日课·第六季(科技高级,¥199/¥365,39.8万,300讲,4.9分,NO.5灰)、沟通心理学实战(心理入门,¥69/¥129,18.6万,24讲,4.6分,NO.6灰)、AI大模型通关指南(科技进阶,¥159/¥299,15.4万,45讲,4.8分,NO.7灰)、高效能人士的7个习惯精讲(职场入门,¥89/¥179,24.1万,28讲,4.7分,NO.8灰)。价格从¥69到¥199,学过人数从15.4万到48.2万,课程排名前3鎏金高亮,分类六色动态着色(职场靛蓝/投资鎏金/心理紫/科技蓝/人文粉/其他绿),现价红字+原价删除线,是本应用首页课程列表设计的核心亮点。


四、课程页:分类横滑与大卡课程列表

课程页以"全部课程"为核心,采用"分类横滑+大卡课程列表"的布局。

课程模块 - 全部课程按热度排序与分类横滑及大卡课程列表含加入学习呼吸按钮

页面顶部第一部分是标题"📚 全部课程 · 按热度排序"加粗13号 margin 下 12。

第二部分是分类横滑,Scroll horizontal 遍历 6 个分类(全部/入门友好/进阶提升/大师课/拼团中/限时特价),每个分类 Text 10 号,选中(c === ‘全部’)时白字 indigo 靛蓝底,未选中时 sub 灰字白底 border 1px line 色,padding 左右 14 上下 7,圆角 16。分类横滑让用户快速筛选课程类型,选中态靛蓝底高亮。

第三部分是大卡课程列表,遍历 8 门课程,每门课程 Column 包裹 space 10,padding 12,白底圆角16,border 1px line 色,alignItems Start,包含:

  • 课程图标:icon 40号,width 100% height 86 圆角14 dark底 textAlign Center。
  • 课程名:title title深蓝字13号加粗 maxLines 1。
  • 讲师信息:c.teacher + " · " + c.lessons + “讲 · ⭐” + c.rating(sub灰字9号)。
  • 标签行:Row space 6,分类标签(catColor(c.cat)动态着色8号,chip底圆角6 padding 左右6上下2)+ 等级 + " · " + studentsStr(c.students) + “人报名”(text3灰字8号)。
  • 价格按钮行:Row space 8,现价(red红字15号加粗)+ 原价(text3灰字9号,decoration LineThrough删除线)+ Column layoutWeight 1 + "加入学习"按钮(白字10号加粗,indigo靛蓝底,padding 左右16上下6 圆角12,带 breath 透明度动画 this.breath ? 0.75 : 1,animation duration 500ms)。

8 门课程大卡与首页精品课数据一致,但布局不同——课程页用大卡(图标86px高+课程名+讲师信息+标签+价格按钮行),首页用横向信息卡(图标60px+标题排名+讲师+标签+价格列),两页差异化布局避免重复。"加入学习"按钮的 breath 呼吸动画让核心操作有了"吸引眼球"的动态感,现价红字+原价删除线突出优惠力度,是本应用课程页交互设计的亮点。


五、听书页:音频书大卡与章节列表

听书页以"音频听书"为核心,采用"音频书大卡+播放控制+章节列表+换一本听"的四层布局。

听书模块 - 听书馆精品解读与音频书大卡播放按钮及章节列表换一本听

页面顶部第一部分是标题"🎧 听书馆 · 精品解读"加粗13号 margin 下 12。

第二部分是当前音频书大卡,Row space 12,padding 14,白底圆角16,border 1px line 色:

  • 书籍图标:AUDIO_LIST[this.audioIdx].icon 42号,width 88 height 112 圆角14 dark底 textAlign Center。
  • 书籍信息列(layoutWeight 1,左对齐,space 6):
    • 书名:title title深蓝字14号加粗。
    • 作者播讲:author + " 著 · " + narrator + " 播"(sub灰字9号)。
    • 时长价格:"⏱️ 全书 " + duration + " · " + price(sub灰字9号)。
    • 播放控制行:Row space 10,播放/暂停按钮(audioPlaying ? “⏸️ 暂停” : “▶️ 播放”,白字11号加粗,indigo靛蓝底,padding 左右18上下7 圆角16,带 wave animation duration 600ms,点击切换 audioPlaying 状态)+ "已听 46%"green绿字9号加粗。

第三部分是听书设置行,Row space 8:"🎵 倍速 1.25x"indigo靛蓝字9号,chip底圆角8 padding 左右8上下4 + "🌙 定时 30min"purple紫字9号,dark底圆角8 padding 左右8上下4 + Column layoutWeight 1 + "上次听到 第2章 32:14"text3灰字9号。倍速1.25x和定时30min是音频听书的标准功能,上次听到位置记录体现学习连续性。

第四部分是章节列表,标题"📖 章节列表"加粗12号 + 遍历 AUDIO_LIST[this.audioIdx].chapters 章节数组,每章 Row space 12,padding 11,白底圆角12,alignItems Center,包含:

  • 章节序号:(ci+1).toString() indigo靛蓝字11号加粗,width 26 height 26 圆角13 chip底 textAlign Center。
  • 章节名:ch title深蓝字11号 layoutWeight 1 maxLines 1。
  • 状态时长:ci===1时"正在播"green绿字9号加粗,否则 chapterDur(ci)(章节时长,42:18/38:05/45:30/51:12循环,text3灰字9号)。

当前音频(audioIdx=0)为《置身事内》,4章:第一章地方政府的事权(42:18)/第二章财税与政府行为(正在播,38:05)/第三章政府投融资与债务(45:30)/第四章工业化中的政府角色(51:12)。第二章标记"正在播"green绿字加粗,其他章节显示时长,是本应用听书页章节列表设计的亮点。

第五部分是换一本听,标题"🎧 换一本听"加粗12号 + Flex wrap 遍历 4 本音频书,每本 Column 包裹 space 5,width 23%,padding 8,白底圆角12,margin 4,alignItems Center,包含:

  • 书籍图标:icon 26号,width 60 height 78 圆角10 dark底 textAlign Center。
  • 书名:title title深蓝字9号加粗 maxLines 1。
  • 时长:duration text3灰字8号。
  • 选中态:borderWidth this.audioIdx === a.id-1 ? 2 : 1,borderColor 选中时indigo靛蓝否则line灰,点击切换 audioIdx = a.id-1。

4 本音频书:置身事内(兰小欢著,梁晨播,18小时,¥45,4章)、穷查理宝典(彼得·考夫曼著,老喻播,26小时,¥59,3章)、思考快与慢(丹尼尔·卡尼曼著,徐幼鱼播,31小时,¥69,3章)、纳瓦尔宝典(埃里克·乔根森著,中书君播,9小时,¥39,3章)。换一本听Flex wrap网格布局,当前音频边框2px靛蓝高亮,点击切换audioIdx,章节列表随audioIdx动态更新(ForEach key含audioIdx),是本应用听书页交互设计的核心亮点。播放按钮的wave animation让播放操作有了"微动"反馈,audioPlaying状态控制播放/暂停文字切换,形成了完整的音频听书交互闭环。


六、学习页:时长柱状图与笔记增删改

学习页以"学习数据+笔记管理"为核心,采用"学习数据三卡+每日时长柱状图+笔记列表+学习成就"的四层布局。

学习模块 - 学习数据本周概览三卡与每日学习时长靛蓝→深靛蓝渐变柱状图及笔记列表

页面顶部第一部分是标题"📈 学习数据 · 本周概览"加粗13号 margin 下 12。

第二部分是学习数据三卡,Row space 10,三卡各 layoutWeight 1,padding 12,白底圆角14,alignItems Start:

  • 学习时长卡:"470min"indigo靛蓝字18号加粗 + "学习时长"sub灰字9号。
  • 打卡率卡:"86%"gold鎏金字18号加粗 + "打卡率"sub灰字9号。
  • 学习笔记卡:"32条"green绿字18号加粗 + "学习笔记"sub灰字9号。

三卡用靛蓝/鎏金/绿三色区分学习时长/打卡率/笔记数,与学习头部的连续学习天数三卡(靛蓝/鎏金/绿)色彩呼应,形成统一的学习数据视觉语言。

第三部分是每日学习时长柱状图,Column 包裹 space 10:

  • 标题:"⏱️ 每日学习时长(分钟)"加粗11号。
  • 柱状图 Row space 10:遍历 WEEK_DAYS 7天(一/二/三/四/五/六/日),每栏 alignItems Center,包含:顶部数值(STUDY_MIN[i],sub灰字8号)+ 柱子(width 14,height = studyBarH(v) = Math.round(v / 120 * 90) 动态计算,圆角 7,linearGradient 180 度从 indigo #3D4EB5 靛蓝到 indigoD #28337D 深靛蓝的垂直渐变)+ 底部星期(text3灰字8号)。柱状图卡片 padding 上下10下12,白底圆角14,justifyContent Center。

7 天学习数据:周一45/周二62/周三30/周四88/周五55/周六120(最高)/周日70。周六120分钟最高(周末学习时间多),周三30分钟最低(工作日忙)。柱子高度 = v/120*90 动态计算,以120分钟为基准映射到90px高度,180度靛蓝→深靛蓝垂直渐变让柱子有了"从上到下由浅入深"的视觉感,是本应用数据可视化的核心亮点。

第四部分是学习笔记列表,标题行 Row space 8:"📝 学习笔记 · " + noteList.length + " 条"加粗12号 layoutWeight 1 + "➕ 记笔记"indigo靛蓝字10号加粗(点击触发 addModal 新增笔记弹窗)。

遍历 noteList 6 条笔记,每条笔记 Column 包裹 space 8,padding 12,白底圆角14,border 1px line 色,alignItems Start,包含:

  • 标题行:Row space 8,笔记图标 noteIcon(n.tag) 18号(职场💼/投资📈/心理🧠/科技🤖/其他📖)+ 笔记名(title深蓝字11号加粗 layoutWeight 1)+ 时间(text3灰字8号)。
  • 内容:n.content sub灰字10号 maxLines 2。
  • 操作行:Row space 8,分类标签(catColor(n.tag)动态着色8号,chip底圆角6 padding 左右6上下2)+ Column layoutWeight 1 + "❤️ " + n.likes(pink粉字9号,带 wave animation duration 500ms,点赞心动效)+ "✏️"编辑12号(点击触发 editModal,editIdx=ni,editTitle/editContent预填原笔记)+ "🗑️"删除12号(点击触发 delModal,editIdx=ni)。

6 条笔记覆盖四大分类:沉没成本不是成本(投资,08-20,128赞)、产品要打痛点不打痒点(职场,08-18,96赞)、系统1与系统2(心理,08-15,214赞)、复利的本质是时间(投资,08-12,305赞)、写作是最好的思考(职场,08-10,156赞)、注意力是最稀缺资源(科技,08-06,89赞)。笔记内容都是经典学习知识点,点赞数从89到305,分类六色动态着色,❤️点赞带wave animation心动效,✏️编辑触发editModal(预填原笔记),🗑️删除触发delModal(二次确认),形成了完整的笔记管理增删改闭环,是本应用学习页交互设计的核心亮点。

第五部分是学习成就,Column 包裹 space 8,padding 14,白底圆角14:

  • 标题:"🏅 学习成就"加粗11号。
  • 成就标签行:Row space 10,遍历 4 个成就(🥇 百日坚持/📚 12本/🔥 32条/⭐ 5证书),每个成就 indigo靛蓝字9号,dark底圆角10 padding 左右10上下5。

4 个成就标签用emoji+文字形式展示学习成果,百日坚持/12本完读/32条笔记/5证书,与学习数据三卡和连续学习天数呼应,形成完整的学习成就体系。


七、我的页:用户卡渐变与已购课程进度条

我的页以"个人中心+已购课程+证书"为核心,采用"用户卡+已购课程进度条+证书"的三层布局。

我的模块 - 用户卡深靛蓝→紫渐变与我的课程动态进度条及官方认证证书

页面顶部第一部分是用户卡,Column 包裹 space 10,padding 16,圆角 18,linearGradient 135 度从 indigoD #28337D 深靛蓝到 purple #8A5FC0 紫的两色渐变(与首页 Banner 的深靛蓝→靛蓝不同,我的卡用深靛蓝→紫,增加视觉变化):

  • 用户信息行:Row space 12,🧑‍🎓头像32号(width 58 height 58 圆角29,rgba(255,255,255,0.25)半透明白底 textAlign Center)+ 用户信息列(layoutWeight 1,左对齐):"求知者Leo · 年度会员"白字14号加粗 + "会员至 2027-03-15 · 已连续学习 156 天"白字0.85透明度9号。
  • 会员等级行:Row space 8,“学习力 Lv.7”#FFE082鎏金字9号,rgba(255,255,255,0.2)半透明白底圆角10 padding 左右10上下4 + "超越 98% 的同学"白字0.85透明度9号 + Column layoutWeight 1 + “续费 ¥199 >”#FFE082鎏金字10号加粗。

用户卡的 135 度深靛蓝→紫渐变是本应用最具辨识度的视觉元素之一,与首页 Banner 的深靛蓝→靛蓝渐变形成"同色系不同终点"的视觉变化(首页用靛蓝,我的用紫),半透明白底的头像和等级标签在深色渐变上清晰可读,鎏金字的学习力等级和续费按钮突出会员身份,是本应用我的页视觉设计的核心亮点。

第二部分是我的课程标题行,Row space 8:"📖 我的课程 · " + mineList.length + " 门"加粗13号 layoutWeight 1 + "学习报表 >"indigo靛蓝字9号(点击触发 tabIdx=3 跳学习页,实现学习报表跳转)。

第三部分是已购课程列表,遍历 mineList 4 门课程,每门课程 Column 包裹 space 10,padding 12,白底圆角14,border 1px line 色,包含:

  • 课程信息行:Row space 12,课程图标26号(width 52 height 52 圆角14 dark底 textAlign Center)+ 课程信息列(layoutWeight 1,左对齐,space 5):
    • 课程名:title title深蓝字11号加粗 maxLines 1。
    • 讲师进度:m.teacher + " · 已学 " + m.done + “/” + m.lessons + " 讲"(sub灰字9号)。
    • 进度条行:Row space 6,背景条(height 5 圆角3 layoutWeight 1 darkL底)+ 进度条(height 5 圆角3,width = m.progress + “%” 动态,背景色 m.progress===100 ? green绿 : indigo靛蓝,已完成绿色否则靛蓝)。
    • 进度文字:"进度 " + m.progress + “%”(text3灰字8号)。
    • 操作按钮:m.progress===100 ? "已完成"green绿字10号加粗 chip底 : "继续学"白字10号加粗 indigo靛蓝底,padding 左右12上下7 圆角10。

4 门已购课程:薛兆丰的经济学课(68%,250/367讲,靛蓝进度条,继续学)、产品思维30讲(100%,30/30讲,绿色进度条,已完成)、精英日课·第六季(42%,126/300讲,靛蓝进度条,继续学)、AI大模型通关指南(15%,7/45讲,靛蓝进度条,继续学)。进度条width=progress%动态计算,已完成(progress=100)进度条绿色否则靛蓝,操作按钮已完成绿色chip底否则靛蓝白底,两态区分清晰,是本应用我的页进度条设计的核心亮点。"学习报表"按钮点击跳学习页(tabIdx=3),实现页面间导航跳转。

第四部分是我的证书,标题"📜 我的证书"加粗13号 + Flex wrap 遍历 3 个证书(经济学结业/产品思维认证/AI通识课),每个证书 Column 包裹 space 5,width 31%,padding 12,白底圆角14,margin 4,alignItems Center,border 1px line 色,包含:

  • 证书图标:🏅26号。
  • 证书名:c title深蓝字9号加粗。
  • 认证标识:"官方认证"gold鎏金字8号。

3 个证书覆盖经济学/产品思维/AI三大领域,🏅图标+证书名+官方认证鎏金字,形成完整的学习证书体系,与学习成就和连续学习天数呼应,激励用户持续学习。


八、色彩主题系统:靛蓝学术主题与三种渐变视觉语言

8.1 色彩接口定义

任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个 ColorPalette 接口,将整个应用所需的色彩抽象为一个结构化的契约,共 21 个字段:

interface ColorPalette {
  bg: string; card: string; dark: string; darkL: string;
  title: string; sub: string; text3: string;
  indigo: string; indigoD: string; gold: string;
  red: string; green: string; blue: string; purple: string; pink: string;
  line: string; tabOn: string; mask: string; chip: string;
}
  • bg:页面整体背景色 #EEF0FB,靛蓝学术底,营造知识付费的学术沉稳氛围。
  • indigo:主色调 #3D4EB5,靛蓝,用于按钮、强调文字、激活Tab、柱状图起始色、进度条等高优先级视觉元素,是学术沉稳的标志性色彩(理性、专业、可信赖)。
  • indigoD:深主色 #28337D,深靛蓝,用于渐变结束色、柱状图底部色、Banner渐变起始色,与 indigo 形成同色系深浅对比。
  • gold:辅助色 #C9A227,鎏金,用于累计学习时长、课程排名前3、证书官方认证、会员续费按钮,与靛蓝形成"靛蓝+鎏金"的经典学术配色组合,象征知识的珍贵与价值。
  • title:文字主色 #1E2249,深蓝,用于主标题文字,与靛蓝学术底形成高对比度的同时带有学术沉稳质感(非纯黑,更柔和专业)。
  • mask:遮罩色 rgba(30,34,73,0.55),深蓝半透明,用于弹窗遮罩,与 title 同色系保证遮罩质感统一。
  • chip:标签底色 rgba(61,78,181,0.10),靛蓝 10% 透明度,用于标签、数据卡底色、按钮底色等,与 indigo 同色系保证标签质感统一。

知了学堂编辑笔记弹窗运行效果 - 弹窗叠加学习页显示笔记标题与内容预填及保存修改按钮

8.2 色彩实例化

const COLORS: ColorPalette = {
  bg: '#EEF0FB', card: '#FFFFFF', dark: '#E2E6F7', darkL: '#D2D8F0',
  title: '#1E2249', sub: '#5A608A', text3: '#9BA3C6',
  indigo: '#3D4EB5', indigoD: '#28337D', gold: '#C9A227',
  red: '#E05B4B', green: '#2E9E6C', blue: '#3D7DC8', purple: '#8A5FC0', pink: '#E0719B',
  line: '#DFE3F5', tabOn: '#3D4EB5',
  mask: 'rgba(30,34,73,0.55)', chip: 'rgba(61,78,181,0.10)'
};

主色 #3D4EB5 是一种沉稳的靛蓝,既契合"知识付费在线学习"主题(学术、理性、专业的标志性色彩),又具有可信赖、高质感的气质。辅助色 #C9A227 鎏金与靛蓝形成"靛蓝+鎏金"的经典学术配色组合,是本应用最具辨识度的色彩组合。bg #EEF0FB 靛蓝学术底与主色同暖色系,营造出知识付费的学术沉稳氛围。title #1E2249 深蓝而非纯黑,与靛蓝学术底形成高对比度的同时带有学术沉稳质感,是本应用色彩设计的细致之处。

值得注意的是,本应用的渐变设计是色彩系统的核心亮点——首页 Banner 使用 linearGradient 135 度从 indigoD #28337D 深靛蓝到 indigo #3D4EB5 靛蓝的两色渐变,我的用户卡使用 linearGradient 135 度从 indigoD #28337D 深靛蓝到 purple #8A5FC0 紫的两色渐变(与首页 Banner 的深靛蓝→靛蓝不同,我的卡用深靛蓝→紫,增加视觉变化),学习页每日学习时长柱状图使用 linearGradient 180 度从 indigo #3D4EB5 靛蓝到 indigoD #28337D 深靛蓝的垂直渐变。三种不同角度和配色的靛蓝渐变贯穿整个应用(135度深靛蓝→靛蓝用于Banner/135度深靛蓝→紫用于用户卡/180度靛蓝→深靛蓝用于柱状图),形成了统一而丰富的靛蓝渐变视觉语言,是本应用色彩设计的核心特色。

本应用的动态颜色逻辑也十分丰富——课程分类按 catColor 分六色着色(职场indigo靛蓝/投资gold鎏金/心理purple紫/科技blue蓝/人文pink粉/其他green绿),课程排名按 courseRank 分两档着色(前3gold鎏金/其他text3灰),已购课程进度条按 progress 分两档着色(已完成100%green绿/未完成indigo靛蓝),名师关注按钮按 follow 分两态着色(已关注text3灰字darkL底/未关注白字indigo底),笔记分类按 catColor 分六色着色(与课程分类一致),形成了"以色定分类、以色定排名、以色定进度、以色定关注、以色定笔记分类"的多层语义着色模式。这种将所有色彩集中管理、配合多维度渐变与动态着色的做法保证了全局一致性,降低了维护成本。

知了学堂删除笔记弹窗运行效果 - 弹窗叠加学习页显示删除确认提示及再想想确认删除按钮


九、类型定义:构建严谨的数据契约

应用通过一组带 constructor 的 @Observed 类,为每一类业务对象建立了明确的数据契约。

9.1 名师、课程、音频书、笔记、已购课程元数据

@Observed export class MasterItem {
  id: number; name: string; avatar: string; title: string;
  courseCount: number; students: string; follow: boolean;
  constructor(...) { ... }
}
@Observed export class CourseItem {
  id: number; title: string; teacher: string; cat: string; level: string;
  price: string; origin: string; students: number; lessons: number; rating: string; icon: string;
  constructor(...) { ... }
}
@Observed export class AudioBook {
  id: number; title: string; author: string; narrator: string;
  duration: string; price: string; chapters: string[]; icon: string;
  constructor(...) { ... }
}
@Observed export class StudyNote {
  id: number; title: string; content: string; tag: string; time: string; likes: number;
  constructor(...) { ... }
}
@Observed export class MyCourse {
  id: number; title: string; icon: string; progress: number;
  lessons: number; done: number; teacher: string;
  constructor(...) { ... }
}

MasterItem 描述名师的 ID、姓名、头像、头衔、课程数、学生数、关注状态,用 follow 字段实现关注按钮两态(已关注灰/未关注靛蓝)。CourseItem 描述课程的 ID、标题、讲师、分类、等级、现价、原价、学生数、讲数、评分、图标,用 cat 字段配合 catColor 函数实现分类六色着色,用 price/origin 实现现价红字+原价删除线,用 students 字段配合 studentsStr 函数实现人数格式化(≥1万转X.X万)。AudioBook 描述音频书的 ID、书名、作者、播讲人、时长、价格、章节数组、图标,用 chapters 数组字段实现章节列表动态渲染,用 audioIdx 状态控制当前音频书切换。StudyNote 描述学习笔记的 ID、标题、内容、分类标签、时间、点赞数,用 tag 字段配合 catColor/noteIcon 函数实现分类着色和图标动态映射,支持增删改(unshift新增/splice替换更新/splice删除)。MyCourse 描述已购课程的 ID、标题、图标、进度百分比、总讲数、已学讲数、讲师,用 progress 字段实现进度条 width 动态计算和已完成/未完成两态着色(progress===100绿色否则靛蓝)。

9.2 工具函数:颜色映射、数据计算、增删改逻辑

function catColor(cat: string): string { ... }
function courseLevelTag(l: string): string { ... }
function noteIcon(tag: string): string { ... }
function studentsStr(n: number): string { if (n >= 10000) { return (n / 10000).toFixed(1) + '万'; } return n.toString(); }
function courseRank(id: number): string { return 'NO.' + id.toString(); }
function chapterDur(i: number): string { const durations: string[] = ['42:18', '38:05', '45:30', '51:12']; return durations[i % 4]; }
function studyBarH(v: number): number { return Math.round(v / 120 * 90); }
function updateNote(list: StudyNote[], idx: number, title: string, content: string): void {
  if (idx < 0 || idx >= list.length) { return; }
  const o: StudyNote = list[idx];
  list.splice(idx, 1, new StudyNote(o.id, title.length > 0 ? title : o.title, content.length > 0 ? content : o.content, o.tag, o.time, o.likes));
}

8 个工具函数将颜色映射、数据计算、增删改逻辑从 UI 层抽离,实现数据与逻辑分离:

  • catColor:分类六色着色,职场indigo/投资gold/心理purple/科技blue/人文pink/其他green。
  • courseLevelTag:课程等级标签,入门green/进阶orange/高级red。
  • noteIcon:笔记图标动态映射,职场💼/投资📈/心理🧠/科技🤖/其他📖。
  • studentsStr:人数格式化,≥10000转(n/10000).toFixed(1)+‘万’,否则返回原数字字符串。
  • courseRank:课程排名,返回’NO.'+id,前3鎏金其他灰(在UI层判断id<=3)。
  • chapterDur:章节时长,4个时长循环(42:18/38:05/45:30/51:12),i%4取模。
  • studyBarH:柱状图高度计算,Math.round(v / 120 * 90),以120分钟为基准映射到90px。
  • updateNote:更新笔记,先判断idx越界防御,splice(idx,1,new StudyNote)替换整个对象,保留原id/tag/time/likes,更新title/content(空值保留原值),触发@Observed响应式更新。

所有数据类都带有 constructor 构造函数,使用 new ClassName(...) 方式实例化,配合 @Observed 装饰器实现响应式更新。8 个工具函数将逻辑从 UI 层抽离,实现数据与逻辑分离,特别是 updateNote 采用 splice 替换对象的方式触发 @Observed 响应式更新(直接修改对象属性可能不触发刷新,替换整个对象会触发),是 ArkUI 响应式更新的关键技巧,体现了对框架机制的深入理解。studentsStr 函数实现人数格式化(≥1万转X.X万),studyBarH 函数实现柱状图高度动态计算,chapterDur 函数实现章节时长循环,都是数据计算层的实用工具函数。


十、配置驱动:常量数据与模拟数据规模

10.1 Tab 导航与常量配置

const TAB_LIST: TabMeta[] = [
  { icon: '🎓', label: '首页' }, { icon: '📚', label: '课程' },
  { icon: '🎧', label: '听书' }, { icon: '📈', label: '学习' }, { icon: '👤', label: '我的' }
];
const WEEK_DAYS: string[] = ['一', '二', '三', '四', '五', '六', '日'];
const STUDY_MIN: number[] = [45, 62, 30, 88, 55, 120, 70];

Tab 导航采用 TabMeta 对象数组配置,5 个 Tab(首页🎓/课程📚/听书🎧/学习📈/我的👤),底部 tabBar 通过 ForEach 遍历 TAB_LIST 构建,每个 Tab Column 包裹(图标19号+文字9号,激活态tabOn靛蓝字加粗,非激活text3灰字),layoutWeight 1 等分宽度,padding 上7下6,点击切换tabIdx。底部白底圆角16。5Tab单排布局是知识付费App的经典导航模式,首页发现/课程浏览/听书音频/学习数据/我的中心,五大模块覆盖完整学习闭环。WEEK_DAYS 常量定义7天标签,STUDY_MIN 常量定义每日学习时长数据(45/62/30/88/55/120/70,周六120最高)。

10.2 模拟数据规模

const MASTER_LIST: MasterItem[] = [ new MasterItem(1, '薛兆丰', '👨‍🏫', '北大经济学教授', 6, '48.2万', true), ... ];
const COURSE_LIST: CourseItem[] = [ new CourseItem(1, '薛兆丰的经济学课', '薛兆丰', '投资', '入门', '¥199', '¥365', 482000, 367, '4.9', '📊'), ... ];
const AUDIO_LIST: AudioBook[] = [ new AudioBook(1, '置身事内', '兰小欢', '梁晨', '18小时', '¥45', ['第一章:地方政府的事权', ...], '📘'), ... ];
const NOTE_INIT: StudyNote[] = [ new StudyNote(1, '沉没成本不是成本', '已经付出且无法收回的时间和金钱...', '投资', '08-20', 128), ... ];
const MINE_INIT: MyCourse[] = [ new MyCourse(1, '薛兆丰的经济学课', '📊', 68, 367, 250, '薛兆丰'), ... ];

五组常量数据承载了知了学堂的全部业务信息:MASTER_LIST 5 位名师(薛兆丰/梁宁/刘擎/香帅/万维钢,课程数3-8门,学生数21.3万-48.2万,2位已关注3位未关注)、COURSE_LIST 8 门课程(价格¥69-¥199,原价¥129-¥399,学生数15.4万-48.2万,讲数24-367,评分4.6-4.9,覆盖投资/职场/人文/心理/科技五大分类)、AUDIO_LIST 4 本音频书(置身事内/穷查理宝典/思考快与慢/纳瓦尔宝典,时长9-31小时,价格¥39-¥69,章节数3-4章)、NOTE_INIT 6 条学习笔记(覆盖投资/职场/心理/科技四大分类,点赞数89-305,时间08-06到08-20)、MINE_INIT 4 门已购课程(进度15%-100%,已学7-250讲,1门已完成3门学习中)。所有 @Observed 类数据使用 new ClassName(...) 构造函数实例化,TabMeta 使用对象字面量。8 个工具函数处理颜色映射、数据计算、增删改逻辑,是本应用唯一的逻辑层。


十一、双动画状态与动态颜色:数据驱动的视觉映射与联动动效

应用通过双动画状态和动态颜色逻辑,将业务数据转化为视觉色彩与动态效果,实现"数据即配色、状态即动效"。

11.1 动态颜色逻辑

本应用的动态颜色逻辑分布在多个维度:

  • 课程分类六色着色catColor(cat),职场indigo靛蓝/投资gold鎏金/心理purple紫/科技blue蓝/人文pink粉/其他green绿,六色分类着色让用户在课程列表和笔记列表中一眼判断分类领域。
  • 课程排名两档着色c.id <= 3 ? gold : text3,前3名鎏金高亮,其他灰色,突出畅销榜TOP3。
  • 已购课程进度两态着色m.progress === 100 ? green : indigo,已完成(progress=100)进度条绿色,未完成靛蓝,两态区分学习状态。
  • 名师关注两态着色m.follow ? text3灰字darkL底 : 白字indigo底,已关注灰色弱化,未关注靛蓝突出,引导用户关注。
  • 笔记分类六色着色catColor(n.tag),与课程分类一致,笔记标签六色着色。
  • 订单状态四档着色:本应用无订单模块,但课程价格用red红字突出现价,原价text3灰字+删除线。

11.2 双动画状态联动

aboutToAppear(): void {
  this.timer = setInterval(() => {
    this.breath = !this.breath;
    this.wave = !this.wave;
  }, 650);
}
aboutToDisappear(): void {
  if (this.timer > 0) { clearInterval(this.timer); }
}

aboutToAppear 中启动 setInterval 650ms 同时切换 breath 和 wave 两个布尔动画状态,aboutToDisappear 中判断 timer > 0 时 clearInterval 清理定时器避免内存泄漏。两个动画状态各司其职驱动四种不同的动效:

  • breath 用于课程页"加入学习"按钮透明度动画this.breath ? 0.75 : 1,animation duration 500ms,所有课程的"加入学习"按钮都有呼吸动画,营造出"核心操作吸引眼球"的动态感。
  • wave 用于首页 Banner 📚 图标 animation.animation({ duration: 650, iterations: 1, curve: Curve.EaseInOut }),Banner 右侧的📚书本图标随 wave 状态微动,营造活动氛围,与 88 好书节活动主题契合。
  • wave 用于听书页播放按钮 animation.animation({ duration: 600, iterations: 1 }),听书页的播放/暂停按钮随 wave 状态微动,营造"音频播放"的动感。
  • wave 用于笔记 ❤️ 点赞 animation.animation({ duration: 500, iterations: 1 }),学习页笔记的❤️点赞文字随 wave 状态微动,营造"心动"的点赞反馈感。

两个动画状态(breath/wave)同时驱动四种动效(按钮呼吸/Banner图标微动/播放按钮微动/点赞心动),650ms 周期同步切换,形成整体协调的联动动效体系。breath 专门驱动课程页按钮透明度动画,wave 同时驱动 Banner 图标/播放按钮/点赞文字三种 animation 动效,两个状态分工明确,让静态的页面有了"鲜活"的动态感,是本应用动效设计的核心亮点。audioPlaying 状态控制听书页播放/暂停文字切换(▶️播放/⏸️暂停),形成了完整的音频听书交互动效闭环。


十二、入口组件:应用的骨架与5Tab差异化布局

12.1 组件声明与状态

@Entry
@Component
struct Page819 {
  @State tabIdx: number = 0;
  @State breath: boolean = false;
  @State wave: boolean = false;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = 0;
  @State addTitle: string = '';
  @State addContent: string = '';
  @State editTitle: string = '';
  @State editContent: string = '';
  @State noteList: StudyNote[] = NOTE_INIT;
  @State mineList: MyCourse[] = MINE_INIT;
  @State audioIdx: number = 0;
  @State audioPlaying: boolean = false;
  private timer: number = -1;
}

17 个 @State 状态分六类:导航状态(tabIdx)、动画状态(breath/wave)、弹窗控制状态(addModal/editModal/delModal)、弹窗数据状态(editIdx/addTitle/addContent/editTitle/editContent,editIdx 共享于 editModal 和 delModal 记录笔记索引,addTitle/addContent 用于新增笔记输入,editTitle/editContent 预填原笔记标题和内容)、业务数据状态(noteList/mineList,初始化为常量数组,支持增删改)、音频状态(audioIdx/audioPlaying,控制当前音频书和播放状态)。private 字段(MASTER_LIST/COURSE_LIST/AUDIO_LIST/NOTE_INIT/MINE_INIT/TAB_LIST/WEEK_DAYS/STUDY_MIN/timer)存储静态模拟数据和定时器引用,不触发响应式更新。

12.2 生命周期与双动画联动

aboutToAppear(): void {
  this.timer = setInterval(() => {
    this.breath = !this.breath;
    this.wave = !this.wave;
  }, 650);
}
aboutToDisappear(): void {
  if (this.timer > 0) { clearInterval(this.timer); }
}

aboutToAppear 中启动 setInterval 650ms 同时切换 breath 和 wave 动画状态,实现双动画联动。aboutToDisappear 中判断 timer > 0 时 clearInterval 清理定时器,避免内存泄漏和重复启动。这种"生命周期管理定时器"的模式是 ArkUI 动效的标准实践。

12.3 学习头部(@Builder header)

顶部头部通过 @Builder header() 抽取,Column 包裹,space 12,padding 左右 14 上下 12:

  • 标题行:Row space 10,"🎓"图标 16 号 padding 6 圆角 12 chip 底 + "知了学堂 · 每天听一讲"title 深蓝字 13 号加粗 + Column layoutWeight 1 + "🔍"搜索 18 号 + "🔔"消息 18 号。
  • 连续学习天数三卡:Row space 12,三卡各 layoutWeight 1,padding 12,圆角 14,alignItems Start:
    • 连续学习卡:"156"indigo靛蓝字26号加粗 + "连续学习(天)"sub灰字9号,chip底。
    • 累计学习时长卡:"286h"gold鎏金字26号加粗 + "累计学习时长"sub灰字9号,白底。
    • 完读书籍卡:"12本"green绿字26号加粗 + "完读书籍"sub灰字9号,白底。
  • 今日课程横条:Row space 8,"📖 今日上新:《置身事内》精读版"indigo靛蓝字9号,dark底圆角8 padding 左右8上下4 + "🎁 满200减40"gold鎏金字9号,chip底圆角8 padding 左右8上下4。

学习头部是本应用最具辨识度的视觉元素,连续学习天数三卡(156天靛蓝/286h鎏金/12本绿)让用户一眼看到学习成果,三卡用靛蓝/鎏金/绿三色区分学习天数/时长/完读数,与学习页的学习数据三卡色彩呼应。今日课程横条(今日上新《置身事内》精读版靛蓝字+满200减40鎏金字)体现课程上新和优惠信息,与整体靛蓝学术主题呼应。

12.4 5Tab单排底部导航与Stack弹窗

底部导航通过 @Builder tabBar() 构建,Row space 0,ForEach 遍历 TAB_LIST 5个Tab,每个Tab Column space 3,layoutWeight 1,padding 上7下6:图标19号+文字9号(激活态tabOn靛蓝字加粗,非激活text3灰字),点击切换tabIdx。底部白底圆角16。

build() 使用 Stack 布局,主 Column 包含 header + Scroll(if-else 切换 5 个 @Builder 页面:tabIdx 0-4 对应 homeTab/courseTab/audioTab/studyTab/mineTab)+ tabBar,width/height 100%,bg 靛蓝学术底。三个弹窗条件渲染:每个弹窗独立 Stack 叠加,共享 modalOverlay 统一遮罩封装(全屏 mask 遮罩 + 点击关闭)。


十三、关键技术特性对比

特性维度 实现方式 技术要点 设计价值
色彩管理 ColorPalette 接口 + COLORS 常量 集中定义 21 种语义色,含靛蓝学术主题色 保证视觉一致性,支持主题切换
靛蓝学术主题 bg #EEF0FB + indigo #3D4EB5 + gold #C9A227 靛蓝学术底+靛蓝主色+鎏金辅助,title深蓝非纯黑 学术沉稳氛围,靛蓝+鎏金经典配色
三种靛蓝渐变 三种不同角度 linearGradient Banner135度深靛蓝→靛蓝/用户卡135度深靛蓝→紫/柱状图180度靛蓝→深靛蓝 统一而丰富的渐变视觉语言
学习头部 连续学习天数三卡+今日课程横条 156天靛蓝/286h鎏金/12本绿三卡,今日上新+满减 学习成果一目了然,课程上新提醒
5Tab差异化 每页独立 @Builder 布局 首页名师横滑+分类网格+精品课/课程大卡列表/听书音频书+章节/学习柱状图+笔记/我的已购+证书 一页一型,还原真实知识付费App
名师横滑 Scroll horizontal + 关注两态 5位名师,已关注text3灰darkL底/未关注白字indigo底 快速发现领域大咖,关注状态清晰
分类网格跳转 Flex wrap + onClick tabIdx=1 8个分类,noteIcon动态图标,点击跳课程页 分类导航一键跳转
分类六色着色 catColor动态函数 职场indigo/投资gold/心理purple/科技blue/人文pink/其他green 以色定分类,一眼判断领域
课程排名两档 id<=3 ? gold : text3 NO.X排名,前3鎏金高亮其他灰 突出畅销榜TOP3
现价红字+删除线 red价格 + decoration LineThrough 现价red红字加粗,原价text3灰字删除线 突出优惠力度,刺激购买
人数格式化 studentsStr函数 ≥10000转(n/10000).toFixed(1)+‘万’ 大数字优雅展示
大卡课程列表 Column大卡 + 加入学习呼吸按钮 图标86px高+课程名+讲师+标签+价格按钮,breath opacity 0.75/1 课程信息完整,核心操作呼吸吸引
音频听书 audioIdx/audioPlaying状态 4本音频书切换,播放/暂停文字切换,章节列表随audioIdx更新 完整音频听书交互闭环
章节列表 ForEach chapters数组 序号+章节名+正在播green/时长text3,chapterDur循环 章节结构清晰,播放进度标记
换一本听 Flex wrap + 选中态边框 4本音频书网格,当前audioIdx边框2px靛蓝,点击切换 快速切换音频书,选中态清晰
学习数据三卡 靛蓝/鎏金/绿三色 470min靛蓝/86%鎏金/32条绿,与头部三卡色彩呼应 学习数据一目了然,色彩统一
每日时长柱状图 studyBarH动态计算 + 180度渐变 v/120*90高度,180度靛蓝→深靛蓝垂直渐变 学习趋势直观,渐变视觉统一
笔记增删改 addModal/editModal/delModal 新增unshift新笔记/编辑updateNote splice替换/删除splice按索引 完整笔记管理闭环
弹窗共享editIdx editModal和delModal共享索引 editIdx记录要编辑/删除的笔记索引,减少重复状态 状态复用,工程化设计
splice替换对象更新 updateNote用splice替换 保留原id/tag/time/likes,更新title/content,替换整个对象触发响应式 ArkUI响应式更新关键技巧
笔记❤️点赞动效 wave animation duration 500ms 点赞文字随wave状态微动,心动反馈 社交交互动效
已购课程进度条 width=progress%动态 + 两态着色 已完成100%green绿/未完成indigo靛蓝,操作按钮两态 学习进度直观,完成状态清晰
用户卡深靛蓝→紫渐变 135度 indigoD→purple 与Banner深靛蓝→靛蓝不同,我的卡用紫增加视觉变化 同色系不同终点,视觉丰富
学习报表跳转 onClick tabIdx=3 我的页"学习报表"按钮点击跳学习页 页面间导航跳转
双动画状态联动 breath+wave两个@State setInterval 650ms同时切换,breath驱动按钮呼吸,wave驱动Banner/播放/点赞微动 两状态驱动四动效,协调联动
三大弹窗 独立 Stack 叠加 + modalOverlay 新增笔记/编辑笔记/删除笔记,共享遮罩样式,editIdx共享索引 完整增删改交互闭环
状态管理 @State + @Observed + private 17个@State分六类,private存静态数据不触发更新 细粒度状态控制,性能优化
组件化 @Component + @Builder 1个入口组件 + 5个页面@Builder + 3个弹窗 + header/tabBar/modalOverlay 高内聚低耦合,代码复用率高
列表渲染 ForEach + 网格/横滑/列表 名师横滑/分类网格/课程列表/音频书网格/章节列表/笔记列表/已购课程/证书网格 数据驱动渲染,布局灵活
数据可视化 柱状图 每日学习时长180度靛蓝→深靛蓝渐变柱状图 学习趋势直观呈现
文本省略 maxLines + textOverflow 课程名maxLines1/笔记内容maxLines2/章节名maxLines1 长文本优雅截断,布局整齐
生命周期 aboutToAppear/aboutToDisappear setInterval启动/clearInterval清理,timer判>0 健壮性强,无资源泄漏
布局系统 Column/Row + Stack + Scroll + Flex 弹性布局、Stack叠加、Scroll纵向滚动、Flex wrap换行 自适应布局,层次分明

完整代码

// ============================================================
// 819 知了学堂 · 知识付费在线教育(现代行业:知识付费 / 在线学习)
// 头部样式:学习头部(连续学习天数卡 + 今日课程横条)无动画
// 布局风格:5 个 tab 每个布局完全不同
//   首页=名师横滑+分类网格+精品课列表 / 课程=大卡课程列表
//   听书=音频书+章节列表 / 学习=时长柱状图+笔记列表 / 我的=已购+证书
// 底部 5 tab 单排
// 主题:靛蓝学术底 + 深蓝 + 鎏金
// ============================================================

interface ColorPalette {
  bg: string;
  card: string;
  dark: string;
  darkL: string;
  title: string;
  sub: string;
  text3: string;
  indigo: string;
  indigoD: string;
  gold: string;
  red: string;
  green: string;
  blue: string;
  purple: string;
  pink: string;
  line: string;
  tabOn: string;
  mask: string;
  chip: string;
}

const COLORS: ColorPalette = {
  bg: '#EEF0FB',
  card: '#FFFFFF',
  dark: '#E2E6F7',
  darkL: '#D2D8F0',
  title: '#1E2249',
  sub: '#5A608A',
  text3: '#9BA3C6',
  indigo: '#3D4EB5',
  indigoD: '#28337D',
  gold: '#C9A227',
  red: '#E05B4B',
  green: '#2E9E6C',
  blue: '#3D7DC8',
  purple: '#8A5FC0',
  pink: '#E0719B',
  line: '#DFE3F5',
  tabOn: '#3D4EB5',
  mask: 'rgba(30,34,73,0.55)',
  chip: 'rgba(61,78,181,0.10)'
};

interface TabMeta {
  icon: string;
  label: string;
}

const TAB_LIST: TabMeta[] = [
  { icon: '🎓', label: '首页' },
  { icon: '📚', label: '课程' },
  { icon: '🎧', label: '听书' },
  { icon: '📈', label: '学习' },
  { icon: '👤', label: '我的' }
];

const WEEK_DAYS: string[] = ['一', '二', '三', '四', '五', '六', '日'];
const STUDY_MIN: number[] = [45, 62, 30, 88, 55, 120, 70];

function catColor(cat: string): string {
  if (cat === '职场') { return COLORS.indigo; }
  if (cat === '投资') { return COLORS.gold; }
  if (cat === '心理') { return COLORS.purple; }
  if (cat === '科技') { return COLORS.blue; }
  if (cat === '人文') { return COLORS.pink; }
  return COLORS.green;
}

function courseLevelTag(l: string): string {
  if (l === '入门') { return 'green'; }
  if (l === '进阶') { return 'orange'; }
  return 'red';
}

function noteIcon(tag: string): string {
  if (tag === '职场') { return '💼'; }
  if (tag === '投资') { return '📈'; }
  if (tag === '心理') { return '🧠'; }
  if (tag === '科技') { return '🤖'; }
  return '📖';
}

@Observed export class MasterItem {
  id: number;
  name: string;
  avatar: string;
  title: string;
  courseCount: number;
  students: string;
  follow: boolean;
  constructor(id: number, name: string, avatar: string, title: string, courseCount: number, students: string, follow: boolean) {
    this.id = id; this.name = name; this.avatar = avatar; this.title = title; this.courseCount = courseCount; this.students = students; this.follow = follow;
  }
}

@Observed export class CourseItem {
  id: number;
  title: string;
  teacher: string;
  cat: string;
  level: string;
  price: string;
  origin: string;
  students: number;
  lessons: number;
  rating: string;
  icon: string;
  constructor(id: number, title: string, teacher: string, cat: string, level: string, price: string, origin: string, students: number, lessons: number, rating: string, icon: string) {
    this.id = id; this.title = title; this.teacher = teacher; this.cat = cat; this.level = level;
    this.price = price; this.origin = origin; this.students = students; this.lessons = lessons; this.rating = rating; this.icon = icon;
  }
}

@Observed export class AudioBook {
  id: number;
  title: string;
  author: string;
  narrator: string;
  duration: string;
  price: string;
  chapters: string[];
  icon: string;
  constructor(id: number, title: string, author: string, narrator: string, duration: string, price: string, chapters: string[], icon: string) {
    this.id = id; this.title = title; this.author = author; this.narrator = narrator;
    this.duration = duration; this.price = price; this.chapters = chapters; this.icon = icon;
  }
}

@Observed export class StudyNote {
  id: number;
  title: string;
  content: string;
  tag: string;
  time: string;
  likes: number;
  constructor(id: number, title: string, content: string, tag: string, time: string, likes: number) {
    this.id = id; this.title = title; this.content = content; this.tag = tag; this.time = time; this.likes = likes;
  }
}

@Observed export class MyCourse {
  id: number;
  title: string;
  icon: string;
  progress: number;
  lessons: number;
  done: number;
  teacher: string;
  constructor(id: number, title: string, icon: string, progress: number, lessons: number, done: number, teacher: string) {
    this.id = id; this.title = title; this.icon = icon; this.progress = progress; this.lessons = lessons; this.done = done; this.teacher = teacher;
  }
}

const MASTER_LIST: MasterItem[] = [
  new MasterItem(1, '薛兆丰', '👨‍🏫', '北大经济学教授', 6, '48.2万', true),
  new MasterItem(2, '梁宁', '👩‍🏫', '产品思维导师', 3, '32.7万', true),
  new MasterItem(3, '刘擎', '🧑‍🏫', '华东师大哲学教授', 4, '27.5万', false),
  new MasterItem(4, '香帅', '👩‍💼', '金融学者', 5, '21.3万', false),
  new MasterItem(5, '万维钢', '🧑‍🔬', '科学作家', 8, '39.8万', false)
];

const COURSE_LIST: CourseItem[] = [
  new CourseItem(1, '薛兆丰的经济学课', '薛兆丰', '投资', '入门', '¥199', '¥365', 482000, 367, '4.9', '📊'),
  new CourseItem(2, '产品思维30讲', '梁宁', '职场', '进阶', '¥99', '¥199', 327000, 30, '4.8', '🧩'),
  new CourseItem(3, '西方现代思想40讲', '刘擎', '人文', '入门', '¥129', '¥259', 275000, 40, '4.9', '🏛️'),
  new CourseItem(4, '香帅中国财富报告', '香帅', '投资', '进阶', '¥199', '¥399', 213000, 52, '4.7', '💰'),
  new CourseItem(5, '精英日课·第六季', '万维钢', '科技', '高级', '¥199', '¥365', 398000, 300, '4.9', '🔭'),
  new CourseItem(6, '沟通心理学实战', '李松蔚', '心理', '入门', '¥69', '¥129', 186000, 24, '4.6', '🧠'),
  new CourseItem(7, 'AI大模型通关指南', '卓克', '科技', '进阶', '¥159', '¥299', 154000, 45, '4.8', '🤖'),
  new CourseItem(8, '高效能人士的7个习惯精讲', '古典', '职场', '入门', '¥89', '¥179', 241000, 28, '4.7', '🚀')
];

const AUDIO_LIST: AudioBook[] = [
  new AudioBook(1, '置身事内', '兰小欢', '梁晨', '18小时', '¥45', ['第一章:地方政府的事权', '第二章:财税与政府行为', '第三章:政府投融资与债务', '第四章:工业化中的政府角色'], '📘'),
  new AudioBook(2, '穷查理宝典', '彼得·考夫曼', '老喻', '26小时', '¥59', ['第一讲:芒格的生活学习决策方法', '第二讲:论基本的普世智慧', '第三讲:论学院派经济学'], '📗'),
  new AudioBook(3, '思考,快与慢', '丹尼尔·卡尼曼', '徐幼鱼', '31小时', '¥69', ['第一部分:两个系统', '第二部分:启发法与偏见', '第三部分:过度自信与决策错误'], '📕'),
  new AudioBook(4, '纳瓦尔宝典', '埃里克·乔根森', '中书君', '9小时', '¥39', ['第一章:积累财富', '第二章:理解幸福', '第三章:学习判断力'], '📙')
];

const NOTE_INIT: StudyNote[] = [
  new StudyNote(1, '沉没成本不是成本', '已经付出且无法收回的时间和金钱,不应影响当下决策,向前看才是理性选择。', '投资', '08-20', 128),
  new StudyNote(2, '产品要打痛点不打痒点', '梁宁讲的情绪→欲望→恐惧三层模型,恐惧才是最强购买动机。', '职场', '08-18', 96),
  new StudyNote(3, '系统1与系统2', '直觉快但易错,理性慢但准确,重要决策强制自己慢三秒。', '心理', '08-15', 214),
  new StudyNote(4, '复利的本质是时间', '年化10%坚持30年是17倍,大多数输在中途下车。', '投资', '08-12', 305),
  new StudyNote(5, '写作是最好的思考', '写不清楚就是没想清楚,输出倒逼输入。', '职场', '08-10', 156),
  new StudyNote(6, '注意力是最稀缺资源', '信息过载时代,过滤信息比获取信息更重要。', '科技', '08-06', 89)
];

const MINE_INIT: MyCourse[] = [
  new MyCourse(1, '薛兆丰的经济学课', '📊', 68, 367, 250, '薛兆丰'),
  new MyCourse(2, '产品思维30讲', '🧩', 100, 30, 30, '梁宁'),
  new MyCourse(3, '精英日课·第六季', '🔭', 42, 300, 126, '万维钢'),
  new MyCourse(4, 'AI大模型通关指南', '🤖', 15, 45, 7, '卓克')
];

@Entry
@Component
struct Page819 {
  @State tabIdx: number = 0;
  @State breath: boolean = false;
  @State wave: boolean = false;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = 0;
  @State addTitle: string = '';
  @State addContent: string = '';
  @State editTitle: string = '';
  @State editContent: string = '';
  @State noteList: StudyNote[] = NOTE_INIT;
  @State mineList: MyCourse[] = MINE_INIT;
  @State audioIdx: number = 0;
  @State audioPlaying: boolean = false;
  private timer: number = -1;

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.breath = !this.breath;
      this.wave = !this.wave;
    }, 650);
  }

  aboutToDisappear(): void {
    if (this.timer > 0) { clearInterval(this.timer); }
  }

  @Builder
  header() {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Text('🎓').fontSize(16).padding(6).borderRadius(12).backgroundColor(COLORS.chip)
        Text('知了学堂 · 每天听一讲').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('🔍').fontSize(18)
        Text('🔔').fontSize(18)
      }
      .width('100%')

      Row({ space: 12 }) {
        Column({ space: 4 }) {
          Text('156').fontSize(26).fontColor(COLORS.indigo).fontWeight(FontWeight.Bold)
          Text('连续学习(天)').fontSize(9).fontColor(COLORS.sub)
        }
        .layoutWeight(1).padding(12).borderRadius(14).backgroundColor(COLORS.chip)
        .alignItems(HorizontalAlign.Start)

        Column({ space: 4 }) {
          Text('286h').fontSize(26).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
          Text('累计学习时长').fontSize(9).fontColor(COLORS.sub)
        }
        .layoutWeight(1).padding(12).borderRadius(14).backgroundColor(COLORS.card)
        .alignItems(HorizontalAlign.Start)

        Column({ space: 4 }) {
          Text('12本').fontSize(26).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
          Text('完读书籍').fontSize(9).fontColor(COLORS.sub)
        }
        .layoutWeight(1).padding(12).borderRadius(14).backgroundColor(COLORS.card)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')

      Row({ space: 8 }) {
        Text('📖 今日上新:《置身事内》精读版').fontSize(9).fontColor(COLORS.indigo)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.dark)
        Text('🎁 满200减40').fontSize(9).fontColor(COLORS.gold)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.chip)
      }
      .width('100%')
    }
    .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 12 })
  }

  @Builder
  homeTab() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Row({ space: 10 }) {
          Column({ space: 4 }) {
            Text('88好书节 · 全场 5 折起').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('薛兆丰·刘擎·万维钢 大师课同价').fontSize(9).fontColor('rgba(255,255,255,0.85)')
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
          Text('📚').fontSize(34)
            .animation({ duration: 650, iterations: 1, curve: Curve.EaseInOut })
        }
        .width('100%')
        Row({ space: 8 }) {
          Text('已有 120 万人参加').fontSize(9).fontColor('rgba(255,255,255,0.85)')
          Column().layoutWeight(1)
          Text('领券购买 >').fontSize(10).fontColor('#FFE082').fontWeight(FontWeight.Bold)
        }
        .width('100%')
      }
      .width('100%').padding(16).borderRadius(18)
      .linearGradient({ angle: 135, colors: [[COLORS.indigoD, 0], [COLORS.indigo, 1]] })

      Text('⭐ 名师大家').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Scroll() {
        Row({ space: 12 }) {
          ForEach(MASTER_LIST, (m: MasterItem) => {
            Column({ space: 6 }) {
              Text(m.avatar).fontSize(32)
                .width(66).height(66).borderRadius(33).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
              Text(m.name).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Text(m.title).fontSize(8).fontColor(COLORS.sub).maxLines(1)
              Text(m.follow ? '已关注' : '+ 关注').fontSize(9)
                .fontColor(m.follow ? COLORS.text3 : '#FFFFFF').fontWeight(FontWeight.Bold)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 }).borderRadius(10)
                .backgroundColor(m.follow ? COLORS.darkL : COLORS.indigo)
            }
            .width(92).padding(10).borderRadius(14).backgroundColor(COLORS.card)
            .alignItems(HorizontalAlign.Center).borderWidth(1).borderColor(COLORS.line)
          }, (m: MasterItem) => 'm' + m.id.toString())
        }
        .padding({ left: 4, right: 4 })
      }
      .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')

      Text('🗂️ 全部分类').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(['职场', '投资', '心理', '科技', '人文', '管理', '语言', '亲子'], (c: string) => {
          Column({ space: 6 }) {
            Text(noteIcon(c)).fontSize(24)
              .width(46).height(46).borderRadius(14).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
            Text(c).fontSize(10).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          }
          .width('23%').padding(10).borderRadius(14).backgroundColor(COLORS.card).margin(4)
          .alignItems(HorizontalAlign.Center)
          .onClick(() => { this.tabIdx = 1; })
        }, (c: string) => 'c' + c)
      }
      .width('100%')

      Row({ space: 8 }) {
        Text('🏆 精品畅销榜').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('完整榜单 >').fontSize(9).fontColor(COLORS.indigo)
      }
      .width('100%')

      ForEach(COURSE_LIST, (c: CourseItem) => {
        Row({ space: 12 }) {
          Text(c.icon).fontSize(30)
            .width(60).height(60).borderRadius(16).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
          Column({ space: 5 }) {
            Row({ space: 6 }) {
              Text(c.title).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1).layoutWeight(1)
              Text(courseRank(c.id)).fontSize(9).fontColor(c.id <= 3 ? COLORS.gold : COLORS.text3)
                .fontWeight(FontWeight.Bold)
            }
            Text(c.teacher + ' · ' + c.lessons + '讲 · ' + c.rating + '分').fontSize(9).fontColor(COLORS.sub)
            Row({ space: 6 }) {
              Text(c.cat).fontSize(8).fontColor(catColor(c.cat))
                .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.chip)
              Text(c.level).fontSize(8).fontColor(COLORS.text3)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.dark)
            }
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
          Column({ space: 4 }) {
            Text(c.price).fontSize(13).fontColor(COLORS.red).fontWeight(FontWeight.Bold)
            Text(c.origin).fontSize(8).fontColor(COLORS.text3)
              .decoration({ type: TextDecorationType.LineThrough })
            Text(studentsStr(c.students) + '人学过').fontSize(8).fontColor(COLORS.text3)
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
        .alignItems(VerticalAlign.Center).borderWidth(1).borderColor(COLORS.line)
      }, (c: CourseItem) => 'cs' + c.id.toString())
    }
    .width('100%')
  }

  @Builder
  courseTab() {
    Column({ space: 12 }) {
      Text('📚 全部课程 · 按热度排序').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Scroll() {
        Row({ space: 10 }) {
          ForEach(['全部', '入门友好', '进阶提升', '大师课', '拼团中', '限时特价'], (c: string) => {
            Text(c).fontSize(10).fontColor(c === '全部' ? '#FFFFFF' : COLORS.sub)
              .padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(16)
              .backgroundColor(c === '全部' ? COLORS.indigo : COLORS.card)
              .borderWidth(c === '全部' ? 0 : 1).borderColor(COLORS.line)
          }, (c: string) => c)
        }
        .padding({ left: 4, right: 4 })
      }
      .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')

      ForEach(COURSE_LIST, (c: CourseItem) => {
        Column({ space: 10 }) {
          Text(c.icon).fontSize(40)
            .width('100%').height(86).borderRadius(14).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
          Text(c.title).fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1)
          Text(c.teacher + ' · ' + c.lessons + '讲 · ⭐' + c.rating).fontSize(9).fontColor(COLORS.sub)
          Row({ space: 6 }) {
            Text(c.cat).fontSize(8).fontColor(catColor(c.cat))
              .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.chip)
            Text(c.level + ' · ' + studentsStr(c.students) + '人报名').fontSize(8).fontColor(COLORS.text3)
          }
          Row({ space: 8 }) {
            Text(c.price).fontSize(15).fontColor(COLORS.red).fontWeight(FontWeight.Bold)
            Text(c.origin).fontSize(9).fontColor(COLORS.text3)
              .decoration({ type: TextDecorationType.LineThrough })
            Column().layoutWeight(1)
            Column() { Text('加入学习').fontSize(10).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
            .padding({ left: 16, right: 16, top: 6, bottom: 6 }).borderRadius(12)
            .backgroundColor(COLORS.indigo)
            .opacity(this.breath ? 0.75 : 1).animation({ duration: 500, iterations: 1 })
          }
          .width('100%')
        }
        .width('100%').padding(12).borderRadius(16).backgroundColor(COLORS.card)
        .alignItems(HorizontalAlign.Start).borderWidth(1).borderColor(COLORS.line)
      }, (c: CourseItem) => 'cl' + c.id.toString())
    }
    .width('100%')
  }

  @Builder
  audioTab() {
    Column({ space: 12 }) {
      Text('🎧 听书馆 · 精品解读').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Row({ space: 12 }) {
        Text(AUDIO_LIST[this.audioIdx].icon).fontSize(42)
          .width(88).height(112).borderRadius(14).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
        Column({ space: 6 }) {
          Text(AUDIO_LIST[this.audioIdx].title).fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Text(AUDIO_LIST[this.audioIdx].author + ' 著 · ' + AUDIO_LIST[this.audioIdx].narrator + ' 播').fontSize(9).fontColor(COLORS.sub)
          Text('⏱️ 全书 ' + AUDIO_LIST[this.audioIdx].duration + ' · ' + AUDIO_LIST[this.audioIdx].price).fontSize(9).fontColor(COLORS.sub)
          Row({ space: 10 }) {
            Column() {
              Text(this.audioPlaying ? '⏸️ 暂停' : '▶️ 播放').fontSize(11)
                .fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .padding({ left: 18, right: 18, top: 7, bottom: 7 }).borderRadius(16)
            .backgroundColor(COLORS.indigo)
            .animation({ duration: 600, iterations: 1 })
            .onClick(() => { this.audioPlaying = !this.audioPlaying; })
            Text('已听 46%').fontSize(9).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
          }
        }
        .alignItems(HorizontalAlign.Start).layoutWeight(1)
      }
      .width('100%').padding(14).borderRadius(16).backgroundColor(COLORS.card).borderWidth(1).borderColor(COLORS.line)

      Row({ space: 8 }) {
        Text('🎵 倍速 1.25x').fontSize(9).fontColor(COLORS.indigo)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.chip)
        Text('🌙 定时 30min').fontSize(9).fontColor(COLORS.purple)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.dark)
        Column().layoutWeight(1)
        Text('上次听到 第2章 32:14').fontSize(9).fontColor(COLORS.text3)
      }
      .width('100%')

      Text('📖 章节列表').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      ForEach(AUDIO_LIST[this.audioIdx].chapters, (ch: string, ci: number) => {
        Row({ space: 12 }) {
          Text((ci + 1).toString()).fontSize(11).fontColor(COLORS.indigo).fontWeight(FontWeight.Bold)
            .width(26).height(26).borderRadius(13).backgroundColor(COLORS.chip).textAlign(TextAlign.Center)
          Text(ch).fontSize(11).fontColor(COLORS.title).layoutWeight(1).maxLines(1)
          Text(ci === 1 ? '正在播' : chapterDur(ci)).fontSize(9)
            .fontColor(ci === 1 ? COLORS.green : COLORS.text3)
            .fontWeight(ci === 1 ? FontWeight.Bold : FontWeight.Normal)
        }
        .width('100%').padding(11).borderRadius(12).backgroundColor(COLORS.card)
        .alignItems(VerticalAlign.Center)
      }, (ch: string) => 'ch' + this.audioIdx.toString() + ch)

      Text('🎧 换一本听').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(AUDIO_LIST, (a: AudioBook) => {
          Column({ space: 5 }) {
            Text(a.icon).fontSize(26)
              .width(60).height(78).borderRadius(10).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
            Text(a.title).fontSize(9).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1)
            Text(a.duration).fontSize(8).fontColor(COLORS.text3)
          }
          .width('23%').padding(8).borderRadius(12).backgroundColor(COLORS.card).margin(4)
          .alignItems(HorizontalAlign.Center)
          .borderWidth(this.audioIdx === a.id - 1 ? 2 : 1)
          .borderColor(this.audioIdx === a.id - 1 ? COLORS.indigo : COLORS.line)
          .onClick(() => { this.audioIdx = a.id - 1; })
        }, (a: AudioBook) => 'ab' + a.id.toString())
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  studyTab() {
    Column({ space: 12 }) {
      Text('📈 学习数据 · 本周概览').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('470min').fontSize(18).fontColor(COLORS.indigo).fontWeight(FontWeight.Bold)
          Text('学习时长').fontSize(9).fontColor(COLORS.sub)
        }
        .layoutWeight(1).padding(12).borderRadius(14).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)
        Column({ space: 4 }) {
          Text('86%').fontSize(18).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
          Text('打卡率').fontSize(9).fontColor(COLORS.sub)
        }
        .layoutWeight(1).padding(12).borderRadius(14).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)
        Column({ space: 4 }) {
          Text('32条').fontSize(18).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
          Text('学习笔记').fontSize(9).fontColor(COLORS.sub)
        }
        .layoutWeight(1).padding(12).borderRadius(14).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)
      }
      .width('100%')

      Column({ space: 10 }) {
        Text('⏱️ 每日学习时长(分钟)').fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Row({ space: 10 }) {
          ForEach(WEEK_DAYS, (d: string, i: number) => {
            Column({ space: 6 }) {
              Text(STUDY_MIN[i].toString()).fontSize(8).fontColor(COLORS.sub)
              Column().width(14).height(studyBarH(STUDY_MIN[i])).borderRadius(7)
                .linearGradient({ angle: 180, colors: [[COLORS.indigo, 0], [COLORS.indigoD, 1]] })
              Text(d).fontSize(8).fontColor(COLORS.text3)
            }
            .alignItems(HorizontalAlign.Center)
          }, (d: string) => 'sw' + d)
        }
        .width('100%').padding({ top: 10, bottom: 12 }).borderRadius(14).backgroundColor(COLORS.card)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')

      Row({ space: 8 }) {
        Text('📝 学习笔记 · ' + this.noteList.length.toString() + ' 条').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('➕ 记笔记').fontSize(10).fontColor(COLORS.indigo).fontWeight(FontWeight.Bold)
          .onClick(() => { this.addModal = true; })
      }
      .width('100%')

      ForEach(this.noteList, (n: StudyNote, ni: number) => {
        Column({ space: 8 }) {
          Row({ space: 8 }) {
            Text(noteIcon(n.tag)).fontSize(18)
            Text(n.title).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold).layoutWeight(1)
            Text(n.time).fontSize(8).fontColor(COLORS.text3)
          }
          .width('100%')

          Text(n.content).fontSize(10).fontColor(COLORS.sub).maxLines(2)

          Row({ space: 8 }) {
            Text(n.tag).fontSize(8).fontColor(catColor(n.tag))
              .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.chip)
            Column().layoutWeight(1)
            Text('❤️ ' + n.likes).fontSize(9).fontColor(COLORS.pink)
              .animation({ duration: 500, iterations: 1 })
            Text('✏️').fontSize(12)
              .onClick(() => {
                this.editIdx = ni;
                this.editTitle = n.title;
                this.editContent = n.content;
                this.editModal = true;
              })
            Text('🗑️').fontSize(12)
              .onClick(() => { this.delModal = true; this.editIdx = ni; })
          }
          .width('100%')
        }
        .width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
        .alignItems(HorizontalAlign.Start).borderWidth(1).borderColor(COLORS.line)
      }, (n: StudyNote) => 'n' + n.id.toString())

      Column({ space: 8 }) {
        Text('🏅 学习成就').fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Row({ space: 10 }) {
          ForEach(['🥇 百日坚持', '📚 12本', '🔥 32条', '⭐ 5证书'], (b: string) => {
            Text(b).fontSize(9).fontColor(COLORS.indigo)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(10).backgroundColor(COLORS.dark)
          }, (b: string) => b)
        }
        .width('100%').justifyContent(FlexAlign.Center)
      }
      .width('100%').padding(14).borderRadius(14).backgroundColor(COLORS.card)
    }
    .width('100%')
  }

  @Builder
  mineTab() {
    Column({ space: 12 }) {
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Text('🧑‍🎓').fontSize(32)
            .width(58).height(58).borderRadius(29).backgroundColor('rgba(255,255,255,0.25)').textAlign(TextAlign.Center)
          Column({ space: 4 }) {
            Text('求知者Leo · 年度会员').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('会员至 2027-03-15 · 已连续学习 156 天').fontSize(9).fontColor('rgba(255,255,255,0.85)')
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
        }
        .width('100%')
        Row({ space: 8 }) {
          Text('学习力 Lv.7').fontSize(9).fontColor('#FFE082')
            .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
            .backgroundColor('rgba(255,255,255,0.2)')
          Text('超越 98% 的同学').fontSize(9).fontColor('rgba(255,255,255,0.85)')
          Column().layoutWeight(1)
          Text('续费 ¥199 >').fontSize(10).fontColor('#FFE082').fontWeight(FontWeight.Bold)
        }
        .width('100%')
      }
      .width('100%').padding(16).borderRadius(18)
      .linearGradient({ angle: 135, colors: [[COLORS.indigoD, 0], [COLORS.purple, 1]] })

      Row({ space: 8 }) {
        Text('📖 我的课程 · ' + this.mineList.length.toString() + ' 门').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('学习报表 >').fontSize(9).fontColor(COLORS.indigo)
          .onClick(() => { this.tabIdx = 3; })
      }
      .width('100%')

      ForEach(this.mineList, (m: MyCourse) => {
        Column({ space: 10 }) {
          Row({ space: 12 }) {
            Text(m.icon).fontSize(26)
              .width(52).height(52).borderRadius(14).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
            Column({ space: 5 }) {
              Text(m.title).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1)
              Text(m.teacher + ' · 已学 ' + m.done + '/' + m.lessons + ' 讲').fontSize(9).fontColor(COLORS.sub)
              Row({ space: 6 }) {
                Column().height(5).borderRadius(3).layoutWeight(1).backgroundColor(COLORS.darkL)
                Column().height(5).borderRadius(3)
                  .width(m.progress.toString() + '%')
                  .backgroundColor(m.progress === 100 ? COLORS.green : COLORS.indigo)
              }
              .width('100%')
              Text('进度 ' + m.progress.toString() + '%').fontSize(8).fontColor(COLORS.text3)
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)
            Text(m.progress === 100 ? '已完成' : '继续学').fontSize(10).fontWeight(FontWeight.Bold)
              .fontColor(m.progress === 100 ? COLORS.green : '#FFFFFF')
              .padding({ left: 12, right: 12, top: 7, bottom: 7 }).borderRadius(10)
              .backgroundColor(m.progress === 100 ? COLORS.chip : COLORS.indigo)
          }
          .width('100%')
        }
        .width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card).borderWidth(1).borderColor(COLORS.line)
      }, (m: MyCourse) => 'mc' + m.id.toString())

      Text('📜 我的证书').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(['经济学结业', '产品思维认证', 'AI通识课'], (c: string) => {
          Column({ space: 5 }) {
            Text('🏅').fontSize(26)
            Text(c).fontSize(9).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('官方认证').fontSize(8).fontColor(COLORS.gold)
          }
          .width('31%').padding(12).borderRadius(14).backgroundColor(COLORS.card).margin(4)
          .alignItems(HorizontalAlign.Center).borderWidth(1).borderColor(COLORS.line)
        }, (c: string) => 'cert' + c)
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabBar() {
    Row({ space: 0 }) {
      ForEach(TAB_LIST, (t: TabMeta, i: number) => {
        Column({ space: 3 }) {
          Text(t.icon).fontSize(19)
          Text(t.label).fontSize(9)
            .fontColor(this.tabIdx === i ? COLORS.tabOn : COLORS.text3)
            .fontWeight(this.tabIdx === i ? FontWeight.Bold : FontWeight.Normal)
        }
        .layoutWeight(1).padding({ top: 7, bottom: 6 })
        .onClick(() => { this.tabIdx = i; })
      }, (t: TabMeta, i: number) => t.label + i.toString())
    }
    .width('100%').backgroundColor(COLORS.card).borderRadius(16)
  }

  @Builder
  modalOverlay(onClose: () => void) {
    Column().width('100%').height('100%')
      .backgroundColor(COLORS.mask)
      .onClick(() => { onClose(); })
  }

  build() {
    Stack({ alignContent: Alignment.Center }) {
      Column() {
        this.header()
        Scroll() {
          Column({ space: 10 }) {
            if (this.tabIdx === 0) { this.homeTab() }
            else if (this.tabIdx === 1) { this.courseTab() }
            else if (this.tabIdx === 2) { this.audioTab() }
            else if (this.tabIdx === 3) { this.studyTab() }
            else { this.mineTab() }
          }
          .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 16 })
        }
        .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
        this.tabBar()
      }
      .width('100%').height('100%')

      if (this.addModal) {
        Stack({ alignContent: Alignment.Center }) {
          this.modalOverlay(() => { this.addModal = false; })
          Column({ space: 12 }) {
            Text('📝 新增学习笔记').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('记录今天最有收获的知识点').fontSize(10).fontColor(COLORS.sub)
            TextInput({ placeholder: '笔记标题', text: this.addTitle })
              .onChange((v: string) => { this.addTitle = v; })
              .fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
            TextInput({ placeholder: '笔记内容(一句话总结)', text: this.addContent })
              .onChange((v: string) => { this.addContent = v; })
              .fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
            Row({ space: 10 }) {
              Column() { Text('取消').fontSize(12).fontColor(COLORS.sub) }
              .layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.darkL)
              .onClick(() => { this.addModal = false; })
              Column() { Text('保存笔记').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
              .layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.indigo)
              .onClick(() => {
                this.noteList.unshift(new StudyNote(
                  Date.now(),
                  this.addTitle.length > 0 ? this.addTitle : '今日一得',
                  this.addContent.length > 0 ? this.addContent : '坚持每天记录一点。',
                  '职场', '08-21', 0));
                this.addModal = false;
              })
            }
            .width('100%')
          }
          .width('82%').constraintSize({ maxHeight: '80%' }).padding(16).borderRadius(16).backgroundColor(COLORS.card)
        }
        .width('100%').height('100%').zIndex(999)
      }

      if (this.editModal) {
        Stack({ alignContent: Alignment.Center }) {
          this.modalOverlay(() => { this.editModal = false; })
          Column({ space: 12 }) {
            Text('✏️ 编辑笔记').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            TextInput({ placeholder: '笔记标题', text: this.editTitle })
              .onChange((v: string) => { this.editTitle = v; })
              .fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
            TextInput({ placeholder: '笔记内容', text: this.editContent })
              .onChange((v: string) => { this.editContent = v; })
              .fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
            Row({ space: 10 }) {
              Column() { Text('取消').fontSize(12).fontColor(COLORS.sub) }
              .layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.darkL)
              .onClick(() => { this.editModal = false; })
              Column() { Text('保存修改').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
              .layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.indigo)
              .onClick(() => {
                updateNote(this.noteList, this.editIdx, this.editTitle, this.editContent);
                this.editModal = false;
              })
            }
            .width('100%')
          }
          .width('82%').constraintSize({ maxHeight: '80%' }).padding(16).borderRadius(16).backgroundColor(COLORS.card)
        }
        .width('100%').height('100%').zIndex(999)
      }

      if (this.delModal) {
        Stack({ alignContent: Alignment.Center }) {
          this.modalOverlay(() => { this.delModal = false; })
          Column({ space: 14 }) {
            Text('🗑️ 删除这条笔记?').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('删除后不可恢复,请谨慎操作。').fontSize(10).fontColor(COLORS.sub)
            Row({ space: 10 }) {
              Column() { Text('再想想').fontSize(12).fontColor(COLORS.sub) }
              .layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.darkL)
              .onClick(() => { this.delModal = false; })
              Column() { Text('确认删除').fontSize(12).fontColor('#FFFFFF') }
              .layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.red)
              .onClick(() => {
                this.noteList.splice(this.editIdx, 1);
                this.delModal = false;
              })
            }
            .width('100%')
          }
          .width('78%').constraintSize({ maxHeight: '80%' }).padding(16).borderRadius(16).backgroundColor(COLORS.card)
        }
        .width('100%').height('100%').zIndex(999)
      }
    }
    .width('100%').height('100%').backgroundColor(COLORS.bg)
  }
}

function studentsStr(n: number): string {
  if (n >= 10000) {
    return (n / 10000).toFixed(1) + '万';
  }
  return n.toString();
}

function courseRank(id: number): string {
  return 'NO.' + id.toString();
}

function chapterDur(i: number): string {
  const durations: string[] = ['42:18', '38:05', '45:30', '51:12'];
  return durations[i % 4];
}

function studyBarH(v: number): number {
  return Math.round(v / 120 * 90);
}

function updateNote(list: StudyNote[], idx: number, title: string, content: string): void {
  if (idx < 0 || idx >= list.length) { return; }
  const o: StudyNote = list[idx];
  list.splice(idx, 1, new StudyNote(
    o.id,
    title.length > 0 ? title : o.title,
    content.length > 0 ? content : o.content,
    o.tag, o.time, o.likes));
}


十四、开发环境搭建

14.1 安装 DevEco Studio

DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

DevEco Studio 安装欢迎界面

14.2 选择安装路径

选择目标安装目录,建议安装到非系统盘,确认所需空间后点击「下一步」。

选择安装位置

14.3 配置安装选项

设置环境变量,勾选创建桌面快捷方式、将 bin 文件夹添加到 PATH、添加上下文菜单,但是需要重启一下电脑使环境变量生效。

安装选项配置

14.4 选择项目模板

新建一个空白模板,在模板市场中选择 Empty Ability。

选择 Empty Ability 模板

14.5 配置项目信息

设置 API 为 24 的模板项目,项目名称 kfschool,Compatible SDK 6.1.1(24),设备类型勾选 Phone,点击完成后初始化项目,自动下载相关依赖。

配置项目信息


十五、总结

通过对这款知了学堂知识付费在线教育应用的深度拆解,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、动效设计、数据计算等多维度能力的综合体现。

在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将五个功能页面拆分为独立的 @Builder 构建器,每个页面采用完全不同的布局风格,实现"一页一型"的差异化设计;状态层利用 ArkUI 的 @State、@Observed 机制,实现了数据变化到 UI 刷新的自动响应,特别是弹窗数据状态的精细设计(editIdx 共享于 editModal 和 delModal 记录笔记索引,addTitle/addContent 用于新增笔记输入,editTitle/editContent 预填原笔记标题和内容),三个弹窗共享 editIdx 状态实现笔记索引的传递,减少了重复状态定义,体现了对状态管理的精准理解和弹窗复用设计的工程化思维。音频状态(audioIdx/audioPlaying)独立管理当前音频书和播放状态,实现了音频听书的完整交互闭环。

在数据层面,六个 @Observed 数据类(MasterItem/CourseItem/AudioBook/StudyNote/MyCourse)以 constructor 构造函数方式实例化,完整描述了知了学堂的全部业务信息。特别的是,应用定义了 8 个工具函数(catColor/courseLevelTag/noteIcon/studentsStr/courseRank/chapterDur/studyBarH/updateNote),将颜色映射、数据计算、增删改逻辑从 UI 层抽离,实现数据与逻辑分离,是本应用工程化设计的核心亮点。studyBarH 函数将学习时长动态映射为柱状图像素高度(v/120*90),studentsStr 函数实现人数格式化(≥1万转X.X万),chapterDur 函数实现章节时长循环(4个时长i%4取模),updateNote 采用 splice 替换对象的方式触发 @Observed 响应式更新(因为直接修改 @Observed 对象属性可能不触发响应式刷新,而替换整个对象会触发),这是 ArkUI 响应式更新的关键技巧,体现了对框架机制的深入理解。updateNote 还保留了原笔记的 id/tag/time/likes 字段,仅更新 title/content(空值保留原值),实现了精准的部分更新。

在视觉层面,本应用独创了"靛蓝学术主题"设计——主色 #3D4EB5 靛蓝搭配辅助色 #C9A227 鎏金,bg #EEF0FB 靛蓝学术底,title #1E2249 深蓝而非纯黑,与靛蓝学术底形成高对比度的同时带有学术沉稳质感。应用的渐变设计是色彩系统的核心亮点——首页 Banner 使用 135 度深靛蓝→靛蓝渐变,我的用户卡使用 135 度深靛蓝→紫渐变(与首页 Banner 的深靛蓝→靛蓝不同,我的卡用深靛蓝→紫,增加视觉变化),学习页每日学习时长柱状图使用 180 度靛蓝→深靛蓝垂直渐变,三种不同角度和配色的靛蓝渐变贯穿整个应用,形成了统一而丰富的靛蓝渐变视觉语言。学习头部采用连续学习天数三卡(156天靛蓝字chip底/286h鎏金字白底/12本绿字白底)+今日课程横条(今日上新《置身事内》精读版靛蓝字+满200减40鎏金字),是本应用头部的特色设计。应用的动态颜色逻辑也十分丰富——课程分类六色着色(职场靛蓝/投资鎏金/心理紫/科技蓝/人文粉/其他绿)、课程排名两档着色(前3鎏金/其他灰)、已购课程进度两态着色(已完成绿/未完成靛蓝)、名师关注两态着色(已关注灰/未关注靛蓝)、笔记分类六色着色(与课程分类一致)、现价红字+原价删除线,形成了"以色定分类、以色定排名、以色定进度、以色定关注、以色定笔记分类、以红定价格"的多层语义着色模式。统一的色彩系统是整个应用的视觉基石,靛蓝主色搭配鎏金辅助色,配合靛蓝学术底和三种靛蓝渐变,营造出既学术沉稳又知识珍贵的在线学习氛围。

在交互层面,应用独创了"双动画状态联动"设计——aboutToAppear 中 setInterval 650ms 同时切换 breath 和 wave 两个布尔动画状态,breath 用于课程页"加入学习"按钮 opacity 动画(0.75/1,duration 500ms),wave 用于首页 Banner 📚 图标 animation(duration 650ms EaseInOut)、听书页播放按钮 animation(duration 600ms)、笔记 ❤️ 点赞 animation(duration 500ms),两个动画状态各司其职驱动四种不同的动效,形成整体协调的联动动效体系。"加入学习"按钮的呼吸动画让核心操作有了"吸引眼球"的动态感,Banner 📚 图标微动营造活动氛围,播放按钮微动营造"音频播放"的动感,笔记❤️点赞微动营造"心动"的点赞反馈感,四种动效各司其职,让静态的页面有了"鲜活"的动态感。音频听书是本应用最具特色的交互模块——audioIdx 状态控制当前音频书切换(换一本听Flex wrap网格,当前音频边框2px靛蓝高亮),audioPlaying 状态控制播放/暂停文字切换,章节列表随 audioIdx 动态更新(ForEach key含audioIdx),第二章标记"正在播"green绿字加粗,倍速1.25x和定时30min设置,上次听到位置记录,形成了完整的音频听书交互闭环。笔记增删改三大弹窗(新增unshift/编辑updateNote splice替换/删除splice按索引)共享 editIdx 索引,新增笔记默认tag职场/time 08-21/likes 0,编辑笔记预填原标题和内容,删除笔记二次确认,形成了完整的笔记管理增删改闭环。分类网格点击跳课程页(tabIdx=1)和我的页"学习报表"按钮跳学习页(tabIdx=3)实现了页面间导航跳转,已购课程进度条 width=progress% 动态计算且已完成绿色/未完成靛蓝两态着色,名师关注按钮两态(已关注灰/未关注靛蓝),课程排名前3鎏金高亮,现价红字+原价删除线,都是本应用交互设计的精细之处。

知了学堂应用整体运行效果 - 学习头部连续学习天数三卡与88好书节Banner及名师横滑分类网格精品课列表


标签: HarmonyOS · API 24 · ArkTS · 华为 · 知识付费 · 在线学习 · 音频听书 · 学习笔记 · 移动端开发

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐