HarmonyOS 6效果实现:linearGradient 渐变样式详解:angle 角度设置、colors 二维数组 [[颜色,停止点]] 语法及头部 / 卡片 / 弹框的渐变应用
本文深度解析 词格 LYRICRAFT——一款面向独立音乐人和歌词爱好者的创作社区应用。项目采用 HarmonyOS ArkTS 声明式 UI 框架,单文件架构,无外部依赖,通过 6 大 Tab 页面、十余套弹窗组件、属性动画与过渡效果,完整实现作品广场瀑布流、押韵词典韵脚查询、填词挑战投稿参赛、灵感便签创作管理、我的词稿编辑发布、词人主页合作邀约等核心业务闭环。全文约 8 万字,含 8 张图片。

引言:把心事押进韵脚里
在音乐创作的链条中,歌词往往是最私人也最艰难的一环。作曲可以靠灵感,编曲可以靠技术,但歌词需要把无法言说的情绪拆解成一行行有韵脚的文字。多少人在深夜的备忘录里写下半句歌词,又因为找不到合适的韵脚、想不出好的主题、缺乏同好的反馈而搁置在草稿箱里。
词格 LYRICRAFT 正是为这些"把心事押进韵脚里"的创作者而生的歌词创作社区。它不是一个简单的文本编辑器,而是一个集作品展示、押韵工具、创作挑战、灵感管理、词人社交于一体的完整创作生态。在这里,独立词人可以发布作品到广场接受点赞和评论,可以用押韵词典快速查找同韵词,可以参加填词挑战锻炼创作能力,可以用灵感便签随时记录一闪而过的句子,可以管理自己的词稿从草稿到发布的全流程,还可以在个人主页接收作曲人的合作邀约。
视觉设计上,词格 LYRICRAFT 采用"涂鸦深夜霓虹风"——深紫底(#151026)铺陈深夜的创作氛围,霓虹粉(#FF69B4)作为主强调色如同深夜里最亮的霓虹灯牌,青紫(#66E8FF)作为辅助色如同城市夜景的冷光,荧光黄(#FFE066)如同贴在墙上的便利贴,整体营造出一种"深夜工作室里,霓虹灯照亮草稿纸"的创作氛围。
技术层面,项目选择 HarmonyOS ArkTS 作为开发框架,通过声明式 UI 语法、@State 状态管理、@Builder 自定义构建函数、Stack 叠加弹窗、属性动画和过渡效果,用单文件约 2500 行代码实现了 6 个 Tab 页面和十余套弹窗组件的完整交互。应用严格遵循 ArkTS 规范:无 Blank 组件、无 UI 内变量声明、constraintSize 限高、单子组件 Scroll、接口约束全部对象字面量。
本文将从设计理念、色彩系统、架构设计、数据模型、页面实现、弹窗交互、动画效果、业务逻辑等十三个维度,对词格 LYRICRAFT 进行全方位深度解析。
第一章 项目概述与设计理念
1.1 项目定位
词格 LYRICRAFT 定位为"歌词创作者的一站式创作社区",核心用户为独立音乐人、歌词爱好者、校园创作者、说唱歌手等有歌词创作需求的人群。应用融合作品广场、押韵词典、填词挑战、灵感便签、我的词稿、词人主页六大业务模块,形成从"获取灵感→查询韵脚→参加挑战→记录便签→管理词稿→发布作品→获得合作"的完整创作者旅程闭环。
与传统音乐社区不同,词格 LYRICRAFT 更强调"创作过程"而非"成品消费"——作品广场展示的是词人的创作片段而非完整歌曲,押韵词典是创作时的实时工具,填词挑战用命题激发创作欲,灵感便签捕捉碎片化的灵感瞬间,我的词稿管理从草稿到发布的全流程,词人主页连接词人和作曲人的合作。整个应用围绕"帮助创作者写出更好的歌词"这一核心目标展开。
1.2 设计理念:涂鸦深夜霓虹风
项目的视觉设计灵感来源于深夜的城市街头和创作者的工作台。走进一个独立音乐人的工作室,首先感受到的是深夜的安静、霓虹灯的色彩、墙上贴满的便利贴、草稿纸上涂改的痕迹。词格 LYRICRAFT 将这些创作场景元素抽象为数字化的视觉语言:
- 深紫底(#151026):作为应用主背景色,模拟深夜的暗色调,让创作者的注意力集中在内容上,同时营造出适合深夜创作的氛围
- 霓虹粉(#FF69B4):作为主强调色,用于按钮、选中态、Tab高亮、标题装饰,如同深夜里最醒目的霓虹灯牌,吸引创作者的注意力
- 青紫(#66E8FF):作为辅助强调色,用于已发布状态、播放按钮、成功状态、信息提示,如同城市夜景中的冷色灯光,与霓虹粉形成冷暖对比
- 荧光黄(#FFE066):用于灵感便签、奖励标识、每日一题、热门标识,如同贴在墙上的荧光便利贴,捕捉创作者的灵感瞬间
- 深紫卡片(#1B1630/#221B3B):卡片背景色比主背景略亮,形成层次,模拟深夜工作室里被灯光照亮的区域
这种"深夜感"设计不仅仅停留在色彩层面,更贯穿到交互细节中——点赞爱心的缩放动画、便签的轻微旋转、柱状图的生长动画、弹窗的淡入缩放过渡,每一处动画都柔和缓慢,符合深夜创作的安静调性。
1.3 小红书/即刻风格借鉴
项目在社区体验方面借鉴了内容社区的设计风格:作品瀑布流式布局(作者头像+作品片段+点赞评论分享)、分类标签筛选(全部/都市叙事/意象流/民谣/说唱/校园/童趣)、热度榜单(挑战参赛数排行榜)、限时主题(每日一题/本月词王赛)、灵感便签墙(双列瀑布流+轻微旋转效果)。
1.4 技术选型
| 维度 | 选型 | 说明 |
|---|---|---|
| 开发框架 | HarmonyOS ArkTS | 声明式 UI,原生性能 |
| 架构模式 | 单文件组件化 | 所有代码在一个 .ets 文件中 |
| 状态管理 | @State + 组件回调 | 轻量级,无需额外库 |
| 动画方案 | 属性动画 + 过渡效果 | 缩放/透明度/旋转动画,弹窗过渡 |
| 外部依赖 | 无 | 纯 ArkTS 内置组件 |
| Tab 数量 | 6 个 | 广场/押韵/挑战/便签/作品/我的 |
| 弹窗数量 | 十余套 | 作品抽读/举报/韵脚详情/挑战详情/投稿/新建便签/编辑便签/删除便签/编辑词稿/删除词稿/发布词稿/合作邀约/关于/退出登录 |
第二章 色彩系统与视觉风格
2.1 色彩体系总览
词格 LYRICRAFT 的色彩体系由多个核心色值构成,通过在组件中直接引用色值的方式全局使用:
主背景与卡片色:
- 深紫底
#151026:应用主背景色,深夜暗调 - 深紫卡片
#1B1630:卡片、弹窗、列表项背景 - 更深紫
#221B3B:内嵌卡片、输入框背景、代码块 - 分割线紫
#2E2749:分割线、边框
主强调色:
- 霓虹粉
#FF69B4:主按钮、Tab选中、标题装饰、主操作色 - 青紫
#66E8FF:已发布状态、辅助操作、成功状态、信息提示 - 荧光黄
#FFE066:灵感便签、奖励标识、每日一题、热门标识 - 深灰紫
#4E4670:辅助文字、分割线、占位符
文字色:
- 亮白紫
#EDE9F7:主标题、核心文字 - 浅紫
#C9C1E4:副标题、作品内容、说明文字 - 中紫
#9A8FC9:辅助文字、占位符、时间戳 - 暗紫
#6F6795:次要文字、未选中状态
2.2 色彩层级与对比度
深色主题通过三级背景深度和四级文字明度构建清晰的视觉层级:
背景深度三级:
深紫底 #151026——页面主背景,最深最暗深紫卡片 #1B1630——卡片、弹窗、列表项,比背景略亮形成层次更深紫 #221B3B——内嵌卡片、输入框、代码块,比卡片略亮增加深度
文字明度四级:
亮白紫 #EDE9F7——标题、作品名、核心数据浅紫 #C9C1E4——作品内容、副标题、说明中紫 #9A8FC9——辅助说明、时间戳、占位符暗紫 #6F6795——次要文字、未选中状态
2.3 强调色的使用策略
霓虹粉 #FF69B4 是整个应用的视觉锚点,用于:主按钮(投稿参赛/确认发布/新便签)、选中态标识(Tab选中/分类选中/点赞爱心)、数据高亮(本周码字/风格占比)、装饰元素(每日一题卡片/挑战主题首字/标题字母间距)。
青紫 #66E8FF 作为互补色,用于:已发布状态标签、播放/确认按钮、成功状态(已收藏韵脚/已开启同步)、辅助操作按钮(查看邀约/保存便签)、信息提示。
荧光黄 #FFE066 用于:灵感便签背景(黄色便签)、奖励标识(本月词王赛/热门挑战)、每日一题卡片、发布确认按钮、便签强调色。
三种强调色的搭配形成了"粉为主、青为辅、黄为奖"的色彩节奏,霓虹粉的暖与青紫的冷形成对比,荧光黄的亮在深色背景上格外醒目,既保证了视觉统一性,又提供了足够的状态区分度。
2.4 渐变与光影效果
项目少量使用纯色块而非复杂渐变,保持深夜霓虹的简洁感:
- 每日一题卡片使用纯霓虹粉
#FF69B4背景,深色文字,醒目吸引 - 本月词王赛卡片使用纯荧光黄
#FFE066背景,深色文字,奖励感强 - 输入尾字查询卡片使用纯青紫
#66E8FF背景,深色文字,工具感强 - 柱状图使用纯色填充(霓虹粉/青紫),不使用渐变,保持简洁
灵感便签使用半透明背景色(rgba(255,105,180,0.12) 霓虹粉便签/rgba(255,224,102,0.16) 荧光黄便签/rgba(102,232,255,0.12) 青紫便签)+ 半透明边框,模拟真实便利贴的半透明质感,并通过 rotate 给每张便签添加轻微旋转角度(-1.5°/1°/0.8°),增加手工感和趣味性。
第三章 项目架构与文件组织
3.1 单文件组件化架构
词格 LYRICRAFT 采用"单文件组件化"架构——所有代码在一个 2136.ets 文件中,通过 @Component 装饰器划分为 8 个独立组件(6 Tab + 1 头部 + 1 根入口)。优势:零配置运行、低理解成本、快速原型。代码量约 2500 行,处于单文件架构合理范围。
3.2 组件层次结构
LyricraftApp (@Entry 根组件)
├── LyricraftHeader(头部:LOGO + 搜索 + 每日一题 + 押韵词典)
├── 内容区 Column
│ └── 6 个 Tab 内容组件(6 选 1,通过 contentArea() Builder 分发)
│ ├── SquareContent(广场:风格筛选 + 作品瀑布流 + 作品抽读弹窗 + 举报弹窗 + 点赞Toast)
│ ├── RhymeContent(押韵:韵脚查询 + 十大韵脚列表 + 韵脚详情弹窗 + 收藏Toast)
│ ├── LyricChallengeContent(挑战:本月词王赛 + 挑战列表 + 挑战详情弹窗 + 投稿弹窗 + 投稿Toast)
│ ├── NotesContent(便签:灵感墙双列便签 + 新建/编辑/删除弹窗 + 置顶Toast)
│ ├── WorksContent(作品:状态筛选 + 我的词稿列表 + 编辑/删除/发布弹窗 + 发布Toast)
│ └── MeLyricContent(我的:词人卡片 + 码字柱状图 + 风格占比 + 合作邀约/关于/退出弹窗)
└── 底部 Tab 导航栏(6 项,通过 bottomTabItem() Builder 生成)
根组件管理当前 Tab(activeTab),通过 contentArea() Builder 分发 Tab 内容。每个 Tab 组件负责自身 UI 和局部状态(包括弹窗显隐),通过组件内部 @State 管理,无需跨组件通信。头部组件 LyricraftHeader 为静态展示组件,不包含交互状态。
3.3 @Builder 自定义构建函数
项目使用 2 个 @Builder 方法封装可复用 UI 片段:
contentArea():内容区分发,根据 activeTab 渲染对应 Tab 组件bottomTabItem(icon, label, tab):底部 Tab 项,包含图标缩放动画和文字颜色切换
每个 Tab 组件内部还定义了多个 @Builder 方法封装弹窗内容,如 workDetailModal()、reportModal()、rhymeDetailModal()、detailModal()、submitModal()、newNoteModal()、editNoteModal()、deleteNoteModal()、editModal()、deleteModal()、publishModal()、coopModal()、aboutModal()、logoutModal() 等,以及通用的 modalOverlay(onClose) 半透明遮罩层。
@Builder 减少重复代码,提高可读性,支持参数传递。
3.4 弹框架构设计
弹窗采用"Stack 叠加 + if 条件渲染 + 半透明遮罩"模式,每个 Tab 组件内部管理自身的弹窗显隐状态。十余套弹窗各具特色:
- 作品抽读弹窗(SquareContent):底部弹出式,包含作品标题+作者+风格+行数+作品片段+点赞/分享/举报按钮
- 举报弹窗(SquareContent):居中缩放式,举报原因选择(疑似抄袭洗稿/内容低质灌水/不良信息)+取消/提交举报
- 韵脚详情弹窗(RhymeContent):底部弹出式,韵脚名称+常用示例+押韵例句列表+收藏韵脚/用此韵开写按钮
- 挑战详情弹窗(LyricChallengeContent):底部弹出式,挑战主题+命题+投稿数+截止时间+评审方式+热评投稿+投稿参赛按钮
- 投稿弹窗(LyricChallengeContent):居中缩放式,挑战主题+TextArea输入框+小技巧+再想想/提交投稿
- 新建便签弹窗(NotesContent):居中缩放式,TextArea输入框+颜色说明+取消/贴上墙
- 编辑便签弹窗(NotesContent):居中缩放式,当前内容+TextArea修改+取消/保存
- 删除便签弹窗(NotesContent):居中缩放式,警告图标+撕掉这张便签?+便签内容+留着/撕掉
- 编辑词稿弹窗(WorksContent):居中缩放式,当前词稿名+TextInput输入新名字+取消/保存
- 删除词稿弹窗(WorksContent):居中缩放式,警告图标+删除这份词稿?+词稿信息+保留/删除词稿
- 发布词稿弹窗(WorksContent):居中缩放式,词稿名+作品风格/总行数/发布范围+再改改/确认发布
- 合作邀约弹窗(MeLyricContent):居中缩放式,来自作曲人的邀约列表+查看全部按钮
- 关于弹窗(MeLyricContent):居中缩放式,应用名+版本+slogan+版权信息
- 退出登录弹窗(MeLyricContent):居中缩放式,挥手图标+确定退出登录?+说明+再写一会/退出
所有弹窗统一使用半透明黑色背景(rgba(10,7,22,0.7))+ 居中或底部弹出 + 淡入缩放过渡动画,柔和不突兀,符合深夜创作的安静调性。
第四章 数据模型与全局状态管理
4.1 接口定义体系
项目定义 6 个 TypeScript 接口描述数据模型:
- LyricWork:作品项,包含 id、title、author、lines(行数)、likes(点赞数)、style(风格:都市叙事/意象流/民谣/说唱/校园/童趣)、status(状态:已发布/草稿)
- RhymeGroup:韵脚组,包含 id、vowel(韵名:ang韵/an韵/i韵等)、sample(示例词)、words(词数说明)、count(词数)
- ChallengePost:挑战命题,包含 id、theme(主题)、prompt(命题要求)、days(剩余天数)、entries(投稿数)、joined(是否已投稿)
- NoteCard:便签卡片,包含 id、text(便签内容)、color(颜色:neon霓虹粉/yellow荧光黄/cyan青紫)、date、pinned(是否置顶)
- InkDay:码字日数据,包含 day(星期几)、lines(行数)
- StyleStat:风格统计,包含 name、emoji、percent(百分比)
4.2 全局数据常量
约 8 组全局 const 常量:
- 核心业务数据:LYRIC_WORKS(14首作品)、RHYME_GROUPS(10个韵脚组)、CHALLENGE_POSTS(8个挑战命题)、NOTE_CARDS(8张便签)、INK_WEEK(7天码字数据)、STYLE_STATS(4项风格统计)
- 配置数据:STYLE_FILTERS(7个风格标签)
使用 const 确保数据不可变,所有动态状态通过组件 @State 管理。
4.3 三级状态管理策略
- 第一级 全局常量(const):业务数据和配置,任何组件可直接读取,运行时不可变
- 第二级 组件局部状态(@State):组件内交互状态,如 styleIdx(当前风格筛选)、likedIds(点赞作品ID列表)、showWorkDetail(作品抽读弹窗显隐)、favVowels(收藏韵脚ID列表)、joinedIds(已投稿挑战ID列表)、notes(便签列表,可增删改)、statusIdx(作品状态筛选)、showEdit(编辑弹窗显隐)等
- 第三级 根组件协调状态(@State):activeTab(当前Tab),仅此一个跨组件状态
4.4 全局纯函数
项目定义 6 个全局纯函数处理数据转换和业务逻辑:
- noteBg©:根据便签颜色返回半透明背景色,yellow返回
rgba(255,224,102,0.16)、cyan返回rgba(102,232,255,0.12)、neon返回rgba(255,105,180,0.12) - noteBorder©:根据便签颜色返回半透明边框色,yellow返回
rgba(255,224,102,0.5)、cyan返回rgba(102,232,255,0.4)、neon返回rgba(255,105,180,0.5) - noteAccent©:根据便签颜色返回强调色,yellow返回
#FFE066、cyan返回#66E8FF、neon返回#FF69B4 - workStatusColor(s):根据作品状态返回颜色,已发布返回
#66E8FF、草稿返回#9A8FC9 - inkBar(h):根据码字行数计算柱状图高度,行数×2.6
纯函数相同输入永远产生相同输出,无副作用,易于测试和复用。便签相关的三个纯函数(noteBg/noteBorder/noteAccent)实现了便签颜色系统的集中管理,新增便签颜色只需在三个函数中添加分支即可。
第五章 六大 Tab 页面深度解析
5.1 广场 Tab:风格筛选 + 作品瀑布流 + 作品抽读
广场 Tab 是应用的首页和核心入口,承担"发现作品→阅读作品→点赞互动"的核心流程。页面从上到下依次为:风格筛选标签栏、作品瀑布流列表。
风格筛选标签栏:使用 Flex({ wrap: FlexWrap.Wrap }) 实现换行布局,7 个风格标签(全部/都市叙事/意象流/民谣/说唱/校园/童趣),选中态为霓虹粉背景深色文字,未选中为深紫背景中紫文字,点击切换时带有 200ms 缓出缩放动画。筛选逻辑通过 worksNow() 方法实现,"全部"返回完整 LYRIC_WORKS 数组,其他风格按 style 字段过滤。
作品瀑布流列表:每首作品为一张深紫卡片,包含:作者头像(圆形,首字,青紫/荧光黄背景)+作者名+风格·行数+状态标签(已发布青紫/草稿中紫)、作品片段引用块(更深紫背景,两行省略)+作品名(霓虹粉)、互动栏(点赞爱心♡/评论💬/分享⤴)。点击作品卡片弹出作品抽读弹窗,点击爱心切换点赞状态并弹出点赞Toast,点击分享关闭弹窗并弹出Toast,点击举报弹出举报弹窗。
作品抽读弹窗:底部弹出式半屏弹窗,包含"作品抽读"标题(霓虹粉,字母间距3)+作品名(亮白紫粗体)+作者·风格·行数+作品片段引用块(4行完整歌词,更深紫背景)+点赞/分享/举报三个操作按钮。弹窗带有半透明遮罩,点击遮罩关闭。

5.2 押韵 Tab:韵脚查询 + 十大韵脚列表 + 韵脚详情
押韵 Tab 是应用的核心工具功能,为创作者提供实时韵脚查询服务。页面从上到下依次为:韵脚查询入口卡、十大高频韵脚列表。
韵脚查询入口卡:青紫背景圆角卡片,包含"输入尾字,秒查同韵词"标题(深色粗体)+"如输入「光」→ ang 韵 286 词"说明(深色半透明),模拟韵脚查询工具的入口。
十大高频韵脚列表:10 个韵脚组,每个为一行深紫卡片,左侧为韵名首字(荧光黄圆形背景)+韵名(亮白紫粗体)+示例词(中紫)+词数(霓虹粉小字),中间为韵脚词数进度条(青紫,宽度按count/3.2计算),右侧为收藏星标(已收藏★荧光黄/未收藏☆暗紫)。点击韵脚卡片弹出韵脚详情弹窗,点击星标切换收藏状态。
韵脚详情弹窗:底部弹出式半屏弹窗,包含"韵脚详情"标题(霓虹粉,字母间距3)+韵名(青紫粗体大字)+常用示例+分割线+4条押韵例句(更深紫背景,居中)+收藏韵脚/用此韵开写两个操作按钮。收藏后关闭弹窗并弹出收藏Toast。

5.3 挑战 Tab:本月词王赛 + 挑战列表 + 挑战详情
挑战 Tab 是应用的创作激励功能,通过命题挑战激发创作者的创作欲。页面从上到下依次为:本月词王赛+我的战绩双卡片、进行中的命题列表。
本月词王赛+我的战绩双卡片:左侧荧光黄背景卡片(本月词王赛,投稿即得限定徽章),右侧深紫卡片(我的战绩,投稿6·获奖1),两个卡片横向排列。
进行中的命题列表:8 个挑战命题,每个为一行深紫卡片,左侧为主题首字(霓虹粉圆形背景)+主题名(亮白紫粗体)+状态标签(已投稿青紫/可投稿霓虹粉)+命题要求(中紫,单行省略)+剩余天数·投稿数(暗紫),下方为投稿数进度条(青紫,宽度按entries/9计算,300ms缓出动画)。点击挑战卡片弹出挑战详情弹窗。
挑战详情弹窗:底部弹出式半屏弹窗(constraintSize限高80%),包含"挑战详情"标题(霓虹粉,字母间距3)+挑战主题(亮白紫粗体)+命题(青紫)+分割线+三行信息(已有投稿/截止/评审方式,两端对齐)+热评投稿标题+3条热评投稿(更深紫背景)+投稿参赛按钮(霓虹粉,已投稿变为深灰)。点击投稿参赛关闭详情并弹出投稿弹窗。

5.4 便签 Tab:灵感墙双列便签 + 新建/编辑/删除
便签 Tab 是应用的灵感捕捉功能,帮助创作者随时记录碎片化的灵感。页面从上到下依次为:灵感墙标题栏+新便签按钮、双列便签瀑布流。
灵感墙标题栏:左侧为"📌 灵感墙"标题(亮白紫粗体)+"置顶X条·共X条"说明(暗紫),右侧为荧光黄"+ 新便签"按钮,点击弹出新建便签弹窗。
双列便签瀑布流:使用 Flex({ wrap: FlexWrap.Wrap }) 实现双列自适应布局,8 张便签,每张为半透明背景卡片(通过 noteBg 函数着色)+半透明边框(noteBorder),包含:置顶标识📌+编辑✎+删除🗑操作栏、便签内容(亮白紫,行高18)、日期(暗紫)。每张便签通过 rotate 添加轻微旋转角度(-1.5°/1°/0.8°),模拟真实便利贴贴在墙上的手工感。点击编辑弹出编辑便签弹窗,点击删除弹出删除便签弹窗。

5.5 作品 Tab:状态筛选 + 我的词稿列表 + 编辑/删除/发布
作品 Tab 是应用的词稿管理功能,管理从草稿到发布的全流程。页面从上到下依次为:状态筛选Tab栏、词稿统计栏、我的词稿列表。
状态筛选Tab栏:三个状态标签(全部/草稿/已发布),使用 Row + 下划线 Divider 实现Tab切换,选中态为霓虹粉文字+霓虹粉下划线(宽度50%),未选中为暗紫文字+透明下划线。切换通过 statusIdx 状态管理,筛选逻辑通过 worksNow() 方法实现(同时过滤作者为"速写本"和状态)。
词稿统计栏:左侧为"共X份词稿"(暗紫),右侧为"✔ 已开启云端同步"(青紫)。
我的词稿列表:每份词稿为一行深紫卡片,左侧为状态图标(草稿✎荧光黄/已发布✓青紫,半透明荧光黄圆形背景)+词稿名《标题》(亮白紫粗体)+风格·行数·点赞数(中紫)+发布/编辑时间(暗紫),右侧为编辑✎(青紫)+操作按钮(草稿🚀发布/已发布🗑删除)。点击编辑弹出编辑词稿弹窗,点击发布弹出发布词稿弹窗,点击删除弹出删除词稿弹窗。

5.6 我的 Tab:词人卡片 + 码字柱状图 + 风格占比 + 功能列表
我的 Tab 是个人中心,展示词人信息、创作数据和系统功能。页面从上到下依次为:词人卡片、本周码字行数柱状图、作品风格占比、功能列表(合作邀约/关于词格/退出登录)。
词人卡片:深紫圆角卡片,左侧为词人头像(荧光黄圆形,“速"字),中间为词人名"速写本”(亮白紫粗体)+词龄2年·累计142首(中紫)+徽章:热门词作者·周更达人(霓虹粉)。
本周码字行数柱状图:深紫圆角卡片,标题为"本周码字行数"(亮白紫粗体,字母间距2)+“共90行”(霓虹粉),下方为7根柱状图(周一到周日),柱子高度通过 inkBar(d.lines) 计算(行数×2.6),周五周六周日为霓虹粉、周一到周四为青紫,底部为星期标签。底部说明"周六灵感爆发,单日26行"(暗紫)。
作品风格占比:深紫圆角卡片,标题为"作品风格占比"(亮白紫粗体,字母间距2),下方为4项风格进度条(都市叙事🏙34%/意象流🌫26%/民谣🪕21%/说唱🎤19%),每项为emoji+风格名+百分比+进度条(25%以上霓虹粉/以下青紫,400ms缓出生长动画)。
功能列表:合作邀约(青紫文字+"3条新邀约"霓虹粉+箭头,点击弹出合作邀约弹窗)、关于词格(中紫文字+箭头,点击弹出关于弹窗)、退出登录(霓虹粉文字按钮,深紫背景+边框,点击弹出退出登录弹窗)。

第六章 弹窗交互体系深度解析
6.1 弹窗设计总览
词格 LYRICRAFT 共设计十余套弹窗组件,分布在 6 个 Tab 中,涵盖作品阅读、工具查询、创作投稿、便签管理、词稿管理、个人中心六大类。所有弹窗统一采用"Stack 叠加 + if 条件渲染 + 半透明遮罩"的实现模式,底部弹出式弹窗使用半透明遮罩+底部内容,居中弹窗使用半透明遮罩+居中缩放内容。
6.2 作品抽读弹窗:底部弹出式作品阅读
作品抽读弹窗是广场 Tab 的核心交互组件,采用底部弹出式半屏设计。点击作品卡片后弹出,包含:
- 标题区:“作 品 抽 读”(霓虹粉,字母间距3,营造设计感)
- 作品信息:作品名(亮白紫粗体18px)+作者·风格·行数(中紫11px)
- 作品片段:4行完整歌词引用块(更深紫背景,浅紫文字,左对齐),展示作品的核心内容
- 操作按钮:点赞♥(霓虹粉,已点赞1.1倍缩放动画)/分享⤴(青紫)/举报⚑(中紫),三个按钮横向排列
弹窗使用半透明遮罩(rgba(10,7,22,0.7)),点击遮罩区域关闭弹窗。点赞按钮点击切换点赞状态,分享按钮点击关闭弹窗并弹出分享Toast,举报按钮点击弹出举报弹窗。

6.3 挑战详情弹窗:底部弹出式挑战信息
挑战详情弹窗是挑战 Tab 的核心交互组件,采用底部弹出式半屏设计(constraintSize限高80%防止内容过长)。点击挑战卡片后弹出,包含:
- 标题区:“挑 战 详 情”(霓虹粉,字母间距3)
- 挑战信息:挑战主题(亮白紫粗体17px)+命题(青紫11px)
- 分割线
- 信息列表:三行两端对齐信息(已有投稿X份/截止剩X天/评审方式:点赞数前3进编辑终审)
- 热评投稿:标题+3条热评投稿(更深紫背景,浅紫文字,展示优秀投稿示例)
- 投稿按钮:“✎ 投稿参赛”(霓虹粉,已投稿变为深灰"✓ 已投稿·查看我的")
弹窗使用半透明遮罩,点击遮罩关闭。点击投稿参赛按钮关闭详情弹窗并弹出投稿弹窗。

6.4 投稿弹窗:居中缩放式投稿输入
投稿弹窗是挑战 Tab 的投稿交互组件,采用居中缩放式设计。包含:挑战主题+TextArea输入框(限60字,更深紫背景)+“小技巧:第一句落具体名词,第二句押韵收尾”(暗紫)+再想想/提交投稿两个按钮。提交后清空输入、加入已投稿列表、关闭弹窗并弹出投稿成功Toast。
6.5 便签三弹窗:新建/编辑/删除组合
便签 Tab 提供完整的便签 CRUD 弹窗组合:
- 新建便签弹窗:居中缩放式,TextArea输入框(限50字)+"颜色将随机分配:霓虹粉/荧光黄/青蓝"说明+取消/贴上墙(荧光黄)按钮。创建后 unshift 到 notes 数组顶部,颜色默认为neon。
- 编辑便签弹窗:居中缩放式,当前内容展示+TextArea修改输入框+取消/保存(青紫)按钮。保存后修改对应便签内容。
- 删除便签弹窗:居中缩放式,⚠警告图标+“撕掉这张便签?”+便签内容(单行省略)+“灵感一旦撕掉,就真的没了”+留着(青紫)/撕掉(霓虹粉)按钮。确认后 splice 删除对应便签。
6.6 词稿三弹窗:编辑/删除/发布组合
作品 Tab 提供完整的词稿管理弹窗组合:
- 编辑词稿弹窗:居中缩放式,当前词稿名+TextInput输入新名字+取消/保存(青紫)按钮。
- 删除词稿弹窗:居中缩放式,⚠警告图标+“删除这份词稿?”+词稿信息+“删除后可从回收站找回30天”+保留(青紫)/删除词稿(霓虹粉)按钮。
- 发布词稿弹窗:居中缩放式,词稿名+三行信息(作品风格/总行数/发布范围:广场公开可被合作邀约)+再改改(中紫)/确认发布(荧光黄)按钮。发布后弹出发布成功Toast。
6.7 个人中心弹窗:合作邀约/关于/退出登录
我的 Tab 提供三个个人中心弹窗:
- 合作邀约弹窗:居中缩放式,"合 作 邀 约"标题(霓虹粉,字母间距3)+“来自作曲人的邀约”+3条邀约列表(更深紫背景)+"查看全部3条邀约"按钮(青紫)。
- 关于弹窗:居中缩放式,“词 格"大字(霓虹粉,字母间距4)+版本v1.9·立秋版+分割线+slogan"每个韵脚都是一次心跳,每行词都在等人谱曲”+版权信息。
- 退出登录弹窗:居中缩放式,👋挥手图标+“确定退出登录?”+“未同步草稿将保留在本机”+再写一会(青紫)/退出(深灰)按钮。
第七章 动画与过渡效果
7.1 属性动画体系
项目使用 ArkTS 的 .animation() 属性动画实现多组交互动画:
- Tab 图标缩放:底部 Tab 选中时图标从1倍缩放至1.18倍,220ms缓出
- 风格标签缩放:广场风格筛选标签选中时1.05倍缩放,200ms缓出
- 点赞爱心缩放:已点赞爱心保持1.12倍缩放,切换时240ms缓出
- 作品抽读点赞缩放:弹窗内点赞按钮已点赞1.1倍缩放,240ms缓出
- 柱状图生长:本周码字柱状图从0生长到目标高度,周五周六周日霓虹粉、周一到周四青紫
- 进度条生长:挑战投稿数进度条300ms缓出、风格占比进度条400ms缓出、韵脚词数进度条
- Toast缩放:点赞/收藏/投稿/发布/置顶Toast弹出时1.05倍缩放,240ms缓出
7.2 便签旋转效果
灵感便签使用 rotate({ angle: n.id % 3 === 0 ? -1.5 : (n.id % 3 === 1 ? 1 : 0.8) }) 给每张便签添加轻微旋转角度,根据id模3分为三组(-1.5°/1°/0.8°),模拟真实便利贴贴在墙上的不规则手工感,增加趣味性和真实感。
7.3 半透明遮罩过渡
所有弹窗使用半透明黑色背景(rgba(10,7,22,0.7) 或 0.75),弹窗内容使用深紫卡片背景(#1B1630)+深紫边框(#2E2749),形成清晰的层次。底部弹出式弹窗通过 Stack 叠加+Column布局实现(上方透明可点击区域+下方弹窗内容),居中弹窗通过 justifyContent(FlexAlign.Center) 实现。
7.4 动画设计原则
深夜创作应用的动画设计遵循三个原则:
- 柔和缓慢:所有动画时长在180-400ms之间,缓出曲线(Curve.EaseOut),不使用快速弹跳或剧烈动画
- 缩放为主:主要使用缩放和透明度变化,避免位移动画造成视觉干扰
- 状态反馈:每个可交互元素(爱心/星标/按钮/标签)都有选中态动画,让创作者明确感知操作结果
第八章 作品广场与风格分类
8.1 作品数据结构
词格 LYRICRAFT 的作品库由 LYRIC_WORKS 常量数组定义,包含 14 首作品,每首通过 LyricWork 接口描述 7 个字段:id、title、author、lines(行数)、likes(点赞数)、style(风格)、status(状态:已发布/草稿)。
14 首作品展示了多样化的创作主题和风格:都市叙事(晚高峰的便利店/给旧手机的一封信/外卖备注栏里的诗)、意象流(雾在路灯上写字/候鸟改签了秋天/月亮是夜班的灯)、民谣(南方不需要暖气/地下通道口琴声/潮湿的琴键)、说唱(未接来电Demo词/第七次加载失败/凌晨三点的备忘录)、校园(毕业前的最后一节晚自习)、童趣(阳台上的宇宙飞船)。
8.2 七风格分类体系
作品按 style 字段分为 7 个风格,每个风格有明确的创作定位:
- 都市叙事:以城市生活为主题的叙事性歌词,如便利店、旧手机、外卖备注
- 意象流:以意象叠加为主的诗意歌词,如雾、候鸟、月亮
- 民谣:民谣风格歌词,如南方暖气、口琴声、琴键
- 说唱:说唱风格歌词,如未接来电、加载失败、备忘录
- 校园:校园主题歌词,如晚自习
- 童趣:童趣主题歌词,如宇宙飞船
风格筛选通过 worksNow() 方法实现,"全部"返回完整数组,其他风格按 style 字段 filter。风格标签栏使用 Flex wrap 实现换行布局,7 个标签分两行排列。
8.3 作品瀑布流布局
作品列表使用单列卡片布局(非真正的瀑布流,而是统一宽度的卡片列表),每张作品卡片包含:作者信息行(头像+作者名+风格行数+状态标签)、作品引用块(片段+作品名)、互动栏(点赞/评论/分享)。卡片使用深紫背景(#1B1630)+深紫边框(#2E2749)+14px圆角,形成统一的卡片风格。
作品片段引用块使用更深紫背景(#221B3B)+10px圆角,展示两行作品片段(maxLines 2 + Ellipsis 省略),下方为作品名(霓虹粉,"—— 作品名"格式),模拟歌词引用的视觉效果。
8.4 点赞与互动机制
作品点赞通过 likedIds 数组管理(默认 [2,6]),isLiked(w) 方法判断作品是否在点赞列表中。点击爱心切换点赞状态:未点赞时 push 到 likedIds 并弹出点赞Toast,已点赞时 splice 移除。点赞数显示为 w.likes + 1(已点赞时加1模拟实时计数)。
互动栏还包含评论💬和分享⤴按钮(演示版本为静态展示,实际项目可扩展为评论列表和分享功能)。
第九章 押韵词典与韵脚系统
9.1 韵脚数据结构
押韵词典由 RHYME_GROUPS 常量数组定义,包含 10 个韵脚组,每个通过 RhymeGroup 接口描述 5 个字段:id、vowel(韵名)、sample(示例词)、words(词数说明)、count(词数)。
10 个韵脚覆盖中文歌词常用韵母:ang韵(光/巷/唱/望,286词)、an韵(山/蓝/晚/感,254词)、i韵(雨/里/息/记,312词)、ao韵(老/早/潮/岛,198词)、ong韵(风/梦/空/重,224词)、ou韵(走/楼/秋/口,186词)、ing韵(星/听/影/静,262词)、ai韵(海/来/白/待,208词)、ei韵(飞/追/灰/微,142词)、u韵(路/雾/屋/度,176词)。
9.2 韵脚查询入口
押韵 Tab 顶部提供韵脚查询入口卡(青紫背景),模拟"输入尾字,秒查同韵词"的查询功能。实际项目中可扩展为:用户输入汉字→自动识别韵母→展示同韵词列表→支持按词性/字数/热度筛选。
9.3 韵脚列表与进度条
十大高频韵脚列表使用统一卡片布局,每个韵脚卡片包含:韵名首字(荧光黄圆形背景,44px)+韵名(亮白紫粗体)+示例词(中紫)+词数(霓虹粉小字)、词数进度条(青紫,宽度按 count/3.2 计算,最大约97vp)、收藏星标(已收藏★荧光黄/未收藏☆暗紫)。
进度条通过 (r.count / 3.2).toFixed(0) + 'vp' 动态计算宽度,i韵312词最宽(约97vp),ei韵142词最窄(约44vp),直观展示各韵脚的词量对比。
9.4 韵脚收藏与详情
韵脚收藏通过 favVowels 数组管理(默认 [1,3],即ang韵和i韵),isFav(r) 方法判断韵脚是否在收藏列表中。点击星标切换收藏状态。
点击韵脚卡片弹出韵脚详情弹窗,展示:韵名(青紫大字)+常用示例+4条押韵例句(每条为一句完整的歌词示例,展示该韵脚在实际创作中的用法)+收藏韵脚/用此韵开写两个操作按钮。收藏韵脚后关闭弹窗并弹出收藏Toast,用此韵开写可跳转至创作页面(演示版本为Toast提示)。
第十章 填词挑战与投稿机制
10.1 挑战数据结构
填词挑战由 CHALLENGE_POSTS 常量数组定义,包含 8 个挑战命题,每个通过 ChallengePost 接口描述 6 个字段:id、theme(主题)、prompt(命题要求)、days(剩余天数)、entries(投稿数)、joined(是否已投稿)。
8 个挑战命题展示了多样化的创作约束:城市深夜图鉴(写一句发生在凌晨两点的故事,剩3天,486份投稿)、用天气写分手(整段不出现"分手"二字,剩6天,322份)、给十年前的自己(限12行以内,剩9天,758份)、便利店文学(以一件商品为主角,剩2天,291份)、写一首纯拟声词(只用拟声词完成副歌,剩12天,168份)、反义词情书(每行包含一组反义词,剩5天,434份)、地铁站名串联(用5个真实站名写副歌,剩8天,203份)、十二行月亮(月亮只能出现一次,剩15天,512份)。
10.2 本月词王赛
挑战 Tab 顶部提供本月词王赛卡片(荧光黄背景),展示"投稿即得限定徽章"的奖励机制,激励创作者参与挑战。右侧为我的战绩卡片(深紫背景),展示"投稿6·获奖1"的个人战绩。
实际项目中可扩展为:每月评选词王(基于投稿数+点赞数+编辑评审),颁发限定徽章和奖励,排行榜展示Top10词人。
10.3 挑战列表与进度条
进行中的命题列表使用统一卡片布局,每个挑战卡片包含:主题首字(霓虹粉圆形背景,44px)+主题名(亮白紫粗体)+状态标签(已投稿青紫/可投稿霓虹粉)+命题要求(中紫,单行省略)+剩余天数·投稿数(暗紫)、投稿数进度条(青紫,宽度按 Math.min(c.entries / 9, 100) 计算,300ms缓出动画)。
进度条通过 entries/9 计算百分比(最大100%),给十年前的自己758份投稿最宽(100%),写一首纯拟声词168份最窄(约19%),直观展示各挑战的热度对比。
10.4 投稿机制
挑战投稿通过 joinedIds 数组管理(默认 [1]),isJoined(c) 方法判断挑战是否已投稿。点击挑战卡片弹出挑战详情弹窗,详情中点击"投稿参赛"按钮弹出投稿弹窗。
投稿弹窗包含 TextArea 输入框(限60字)+创作小技巧+提交按钮。提交后:清空输入框、将挑战ID push 到 joinedIds、关闭弹窗并弹出投稿成功Toast(荧光黄背景,“✓ 投稿成功,等待同好点亮”)。已投稿的挑战在列表中显示"已投稿"青紫标签,详情中按钮变为"✓ 已投稿·查看我的"深灰按钮。
挑战详情还展示热评投稿(3条优秀投稿示例),为创作者提供参考和灵感。评审方式为"点赞数前3进编辑终审",形成"投稿→点赞→评审→获奖"的完整挑战闭环。
第十一章 灵感便签与创作管理
11.1 便签数据结构
灵感便签由 NOTE_CARDS 常量数组定义,包含 8 张便签,每个通过 NoteCard 接口描述 5 个字段:id、text(便签内容)、color(颜色:neon霓虹粉/yellow荧光黄/cyan青紫)、date、pinned(是否置顶)。
8 张便签展示了创作者的碎片化灵感:电梯里的香水味是没说出口的再见(neon,置顶,今天07:42)、把"舍不得"拆成三个小节来唱(yellow,置顶,昨天23:18)、副歌押ang韵桥段换成ou韵转冷(cyan,昨天21:03)、灵感红绿灯坏了的十字路口所有人都在等(neon,08-24)、歌名备选《加载中》《缓冲》《转圈的小圈》(yellow,08-23)、第一句要用具体名词开场硬币站台雨刮器(cyan,08-22)、桥段留白四小节只放一句气声(neon,08-20)、写完别急着改睡一觉再看(yellow,08-18)。
11.2 便签颜色系统
便签使用三种颜色,通过三个全局纯函数(noteBg/noteBorder/noteAccent)集中管理:
- neon 霓虹粉:背景
rgba(255,105,180,0.12),边框rgba(255,105,180,0.5),强调色#FF69B4 - yellow 荧光黄:背景
rgba(255,224,102,0.16),边框rgba(255,224,102,0.5),强调色#FFE066 - cyan 青紫:背景
rgba(102,232,255,0.12),边框rgba(102,232,255,0.4),强调色#66E8FF
半透明背景+半透明边框模拟真实便利贴的半透明质感,三种颜色交替排列增加视觉趣味。
11.3 双列便签墙布局
灵感墙使用 Flex({ wrap: FlexWrap.Wrap }) 实现双列自适应布局,每张便签宽度 46%,margin 6px,便签内容包含:操作栏(置顶标识📌+编辑✎+删除🗑)、便签内容(亮白紫12px,行高18)、日期(暗紫9px)。
每张便签通过 rotate({ angle: n.id % 3 === 0 ? -1.5 : (n.id % 3 === 1 ? 1 : 0.8) }) 添加轻微旋转角度,id模3为0时-1.5°、为1时1°、为2时0.8°,模拟真实便利贴贴在墙上的不规则手工感。便签使用半透明背景+半透明边框,在深紫背景上呈现出"贴在墙上"的视觉效果。
11.4 便签 CRUD 操作
便签支持完整的增删改查操作,通过组件 @State 管理 notes 数组(初始为 NOTE_CARDS):
- 新建:点击"+ 新便签"按钮弹出新建弹窗,TextArea输入内容,确认后
notes.unshift({ id: length+1, text, color: 'neon', date: '刚刚', pinned: false })添加到顶部 - 编辑:点击✎图标弹出编辑弹窗,展示当前内容+TextArea修改,确认后修改对应便签内容
- 删除:点击🗑图标弹出删除确认弹窗,确认后
notes.splice(index, 1)删除对应便签 - 置顶:便签数据包含 pinned 字段(默认前两张置顶),置顶便签显示📌标识,实际项目可扩展为置顶排序功能
所有操作修改 notes 数组后 UI 自动刷新,便签墙实时更新。
第十二章 词人主页与合作邀约
12.1 词人卡片
我的 Tab 顶部展示词人卡片(深紫背景+边框),包含:词人头像(荧光黄圆形58px,“速"字)、词人名"速写本”(亮白紫粗体17px)、词龄2年·累计142首(中紫11px)、徽章"热门词作者·周更达人"(霓虹粉10px)。
词人卡片是个人品牌的核心展示,头像+名字+数据+徽章形成完整的词人身份标识。实际项目中可扩展为:头像上传、个人简介、社交媒体链接、粉丝数/关注数等。
12.2 本周码字行数统计
本周码字行数通过 INK_WEEK 常量定义 7 天数据(周一6/周二12/周三4/周四18/周五9/周六26/周日15,共90行)。柱状图展示每天行数,柱子高度通过 inkBar(d.lines) 计算(行数×2.6,最大约67.6vp),周五周六周日为霓虹粉(周末创作高峰)、周一到周四为青紫(工作日),底部为星期标签。底部说明"周六灵感爆发,单日26行"(暗紫)。
码字统计帮助创作者了解自己的创作节奏,发现创作高峰时段,养成持续创作的习惯。实际项目中可扩展为:月度/年度统计、创作趋势图、与其他词人对比等。
12.3 作品风格占比
作品风格占比通过 STYLE_STATS 常量定义 4 项风格(都市叙事🏙34%/意象流🌫26%/民谣🪕21%/说唱🎤19%)。每项风格进度条使用嵌套 Column 实现:外层为背景(#221B3B,高度6px,圆角3px),内层为进度(宽度 percent%,25%以上霓虹粉/以下青紫,高度6px,圆角3px,400ms缓出生长动画)。
风格占比帮助创作者了解自己的创作风格分布,发现擅长的风格和可以尝试的新风格。实际项目中可扩展为:风格演变趋势、与平台平均风格对比、推荐适合的挑战等。
12.4 合作邀约机制
合作邀约是词格 LYRICRAFT 的核心社交功能,连接词人和作曲人。我的 Tab 提供合作邀约入口(青紫文字+"3条新邀约"霓虹粉+箭头),点击弹出合作邀约弹窗,展示来自作曲人的邀约列表:
- 作曲·雾岛:想为《晚高峰的便利店》谱曲
- 编曲·LAM:意象流 demo 求词
- 歌手·陈迟:翻唱合作授权
每条邀约为更深紫背景卡片,展示邀约内容。底部"查看全部3条邀约"按钮(青紫)可跳转至完整邀约列表(演示版本为关闭弹窗)。
合作邀约机制形成"词人发布作品→作曲人发现→发起邀约→合作创作"的完整社交闭环,帮助词人获得合作机会,帮助作曲人找到优秀歌词。实际项目中可扩展为:邀约状态管理(待回复/已接受/已拒绝)、即时通讯、合作作品管理、分成结算等。
十三、开发环境搭建
13.1 环境要求
DevEco Studio 5.0+(支持HarmonyOS 6.1.1)、HarmonyOS SDK API 24、Node.js 18+、至少8GB内存。
13.2 安装DevEco Studio

从华为开发者官网下载最新版本,运行安装包,选择安装路径,勾选创建桌面快捷方式,点击安装。
13.3 配置SDK

首次启动后进入File > Settings > HarmonyOS SDK,勾选API 24及以上版本,点击Apply等待下载完成。
13.4 创建项目

选择Create Project > Empty Ability,配置项目名称、包名、保存路径,选择Compile SDK为API 24,点击Finish。
13.5 运行应用

将1316.ets代码替换到entry/src/main/ets/pages/Index.ets,连接HarmonyOS设备或启动模拟器,点击运行按钮预览。
13.6 预览调试

DevEco Studio提供实时预览功能,编辑器右侧打开Previewer面板,支持多设备尺寸预览、深色模式切换、组件树inspect等调试能力。
总结与展望
项目总结
词格 LYRICRAFT 是一款面向独立音乐人和歌词爱好者的创作社区应用,基于 HarmonyOS ArkTS 声明式 UI 框架开发,单文件约 2500 行代码,无外部依赖。项目实现了 6 大 Tab 页面(广场/押韵/挑战/便签/作品/我的)和十余套弹窗组件,完整覆盖作品广场瀑布流、押韵词典韵脚查询、填词挑战投稿参赛、灵感便签创作管理、我的词稿编辑发布、词人主页合作邀约等核心业务场景。
视觉设计上采用"涂鸦深夜霓虹风"——深紫底(#151026)铺陈深夜创作氛围,霓虹粉(#FF69B4)作为主强调色如同最亮的霓虹灯牌,青紫(#66E8FF)作为辅助色如同城市夜景的冷光,荧光黄(#FFE066)如同贴在墙上的便利贴,构建出一种"深夜工作室里霓虹灯照亮草稿纸"的创作氛围。便签使用半透明背景+轻微旋转效果,模拟真实便利贴的手工感和趣味性。
技术实现上严格遵循 ArkTS 规范:无 Blank 组件、无 UI 内变量声明、constraintSize 限高、单子组件 Scroll、接口约束全部对象字面量。采用单文件组件化架构,通过 @Component 划分为 8 个独立组件,@State 管理局部状态,@Builder 封装可复用 UI 和弹窗,Stack 叠加实现半透明遮罩弹窗,属性动画实现交互动效。
技术亮点
- 单文件零依赖架构:所有代码在一个 2136.ets 文件中,无需配置路由、状态管理库、UI 组件库,开箱即用,降低理解成本和部署门槛
- 6 Tab + 十余弹窗完整交互闭环:从作品发现到韵脚查询、从挑战投稿到灵感记录、从词稿管理到合作邀约,形成完整的创作者旅程闭环,每个业务场景都有对应的弹窗交互
- 涂鸦深夜霓虹风视觉体系:三色强调色(霓虹粉/青紫/荧光黄)+ 三级背景深度 + 四级文字明度,构建出统一、有氛围感的深夜创作视觉语言
- 半透明便签+旋转效果:灵感便签使用半透明背景(rgba)+半透明边框+轻微旋转(-1.5°/1°/0.8°),模拟真实便利贴贴在墙上的手工感,是整个应用最具特色的视觉细节
- 押韵词典工具化设计:十大韵脚组+词数进度条+韵脚详情弹窗+押韵例句展示,将韵脚查询工具化,为创作者提供实时创作辅助
- 填词挑战命题创作:8个多样化命题挑战+投稿机制+热评展示+评审规则,通过命题约束激发创作者的创作欲,形成社区互动
- 纯函数便签颜色系统:三个全局纯函数(noteBg/noteBorder/noteAccent)集中管理便签颜色,新增便签颜色只需在三个函数中添加分支,易于扩展和维护
扩展方向
词格 LYRICRAFT 作为演示项目,仍有多个可扩展方向:
- 真实韵脚数据库:当前韵脚数据为本地 const 常量,可对接后端 API 实现完整的韵脚数据库(2000+韵脚词),支持按词性、字数、热度筛选,支持模糊查询和拼音查询
- 作品详情与评论系统:当前作品抽读弹窗仅展示4行片段,可扩展为完整作品详情页,包含完整歌词、评论列表、点赞收藏、分享转发等功能
- 创作者社交体系:当前合作邀约为静态展示,可扩展为完整的社交体系,包括关注/粉丝、私信聊天、合作邀约状态管理、合作作品共创、分成结算等
- 富文本歌词编辑器:当前便签和投稿为纯文本,可扩展为富文本歌词编辑器,支持分段、标注韵脚、标记主歌/副歌/桥段、和弦标注、节拍标记等专业歌词编辑功能
- AI 创作辅助:可集成 AI 能力,实现韵脚推荐、歌词续写、风格转换、智能润色、主题生成等 AI 辅助创作功能,提升创作效率和质量
- 音频合成与 Demo:可集成文本转语音(TTS)和简单的旋律合成,让创作者可以听到歌词的朗读效果和简单的 Demo 旋律,辅助创作决策
- 离线模式与同步:当前数据为本地 const,可扩展为本地存储+云端同步,支持离线创作、多设备同步、版本历史、回收站等功能
词格 LYRICRAFT 展示了 HarmonyOS ArkTS 在创作社区类应用开发中的强大能力——声明式 UI 的简洁语法、@State 的高效状态管理、Stack 叠加弹窗的灵活实现、属性动画的流畅动效、Previewer 的实时预览,让开发者可以用较少的代码实现丰富的创作工具和社区交互。希望本文能为 HarmonyOS 开发者和音乐创作产品从业者提供有价值的参考。
本文基于 HarmonyOS ArkTS 声明式 UI 框架实战项目撰写,所有代码和图片均来自词格 LYRICRAFT 项目。
附录:完整 ArkTS 源码(2136.ets)
以下为词格 LYRICRAFT 应用的完整 ArkTS 源码,单文件约 2560 行,无外部依赖。将代码完整复制到 DevEco Studio 项目的
entry/src/main/ets/pages/Index.ets中即可运行。
// 2136.ets 词格 LYRICRAFT —— 歌词创作社区 / 押韵词典 / 填词挑战 / 灵感便签
// 涂鸦深夜霓虹风:深紫底 + 霓虹粉 + 青紫 + 荧光黄便利贴
// 遵循要求.md:无 Blank、无 UI 内变量声明、constraintSize 限高、单子组件 Scroll、接口约束全部对象字面量
// ============ 类型定义 ============
interface LyricWork {
id: number;
title: string;
author: string;
lines: number;
likes: number;
style: string;
status: string;
}
interface RhymeGroup {
id: number;
vowel: string;
sample: string;
words: string;
count: number;
}
interface ChallengePost {
id: number;
theme: string;
prompt: string;
days: string;
entries: number;
joined: boolean;
}
interface NoteCard {
id: number;
text: string;
color: string;
date: string;
pinned: boolean;
}
interface InkDay {
day: string;
lines: number;
}
interface StyleStat {
name: string;
emoji: string;
percent: number;
}
// ============ 静态数据 ============
const LYRIC_WORKS: LyricWork[] = [
{ id: 1, title: '晚高峰的便利店', author: '速写本', lines: 24, likes: 1286, style: '都市叙事', status: '已发布' },
{ id: 2, title: '雾在路灯上写字', author: '青盐', lines: 32, likes: 2043, style: '意象流', status: '已发布' },
{ id: 3, title: '给旧手机的一封信', author: '速写本', lines: 28, likes: 1675, style: '都市叙事', status: '已发布' },
{ id: 4, title: '南方不需要暖气', author: '柑橘皮', lines: 20, likes: 987, style: '民谣', status: '已发布' },
{ id: 5, title: '未接来电(Demo 词)', author: '夜行笔记本', lines: 16, likes: 543, style: '说唱', status: '草稿' },
{ id: 6, title: '候鸟改签了秋天', author: '青盐', lines: 36, likes: 2310, style: '意象流', status: '已发布' },
{ id: 7, title: '地下通道口琴声', author: '柑橘皮', lines: 24, likes: 1122, style: '民谣', status: '已发布' },
{ id: 8, title: '第七次加载失败', author: '夜行笔记本', lines: 12, likes: 421, style: '说唱', status: '草稿' },
{ id: 9, title: '月亮是夜班的灯', author: '速写本', lines: 28, likes: 1856, style: '意象流', status: '已发布' },
{ id: 10, title: '毕业前的最后一节晚自习', author: '粉笔灰', lines: 40, likes: 3024, style: '校园', status: '已发布' },
{ id: 11, title: '阳台上的宇宙飞船', author: '粉笔灰', lines: 24, likes: 1467, style: '童趣', status: '已发布' },
{ id: 12, title: '潮湿的琴键', author: '青盐', lines: 20, likes: 876, style: '民谣', status: '草稿' },
{ id: 13, title: '外卖备注栏里的诗', author: '柑橘皮', lines: 32, likes: 2688, style: '都市叙事', status: '已发布' },
{ id: 14, title: '凌晨三点的备忘录', author: '夜行笔记本', lines: 16, likes: 634, style: '说唱', status: '草稿' }
];
const RHYME_GROUPS: RhymeGroup[] = [
{ id: 1, vowel: 'ang 韵', sample: '光 / 巷 / 唱 / 望', words: '共 286 词', count: 286 },
{ id: 2, vowel: 'an 韵', sample: '山 / 蓝 / 晚 / 感', words: '共 254 词', count: 254 },
{ id: 3, vowel: 'i 韵', sample: '雨 / 里 / 息 / 记', words: '共 312 词', count: 312 },
{ id: 4, vowel: 'ao 韵', sample: '老 / 早 / 潮 / 岛', words: '共 198 词', count: 198 },
{ id: 5, vowel: 'ong 韵', sample: '风 / 梦 / 空 / 重', words: '共 224 词', count: 224 },
{ id: 6, vowel: 'ou 韵', sample: '走 / 楼 / 秋 / 口', words: '共 186 词', count: 186 },
{ id: 7, vowel: 'ing 韵', sample: '星 / 听 / 影 / 静', words: '共 262 词', count: 262 },
{ id: 8, vowel: 'ai 韵', sample: '海 / 来 / 白 / 待', words: '共 208 词', count: 208 },
{ id: 9, vowel: 'ei 韵', sample: '飞 / 追 / 灰 / 微', words: '共 142 词', count: 142 },
{ id: 10, vowel: 'u 韵', sample: '路 / 雾 / 屋 / 度', words: '共 176 词', count: 176 }
];
const CHALLENGE_POSTS: ChallengePost[] = [
{ id: 1, theme: '城市深夜图鉴', prompt: '写一句发生在凌晨两点的故事', days: '剩 3 天', entries: 486, joined: true },
{ id: 2, theme: '用天气写分手', prompt: '整段不出现"分手"二字', days: '剩 6 天', entries: 322, joined: false },
{ id: 3, theme: '给十年前的自己', prompt: '限 12 行以内', days: '剩 9 天', entries: 758, joined: false },
{ id: 4, theme: '便利店文学', prompt: '以一件商品为主角', days: '剩 2 天', entries: 291, joined: false },
{ id: 5, theme: '写一首纯拟声词', prompt: '只用拟声词完成副歌', days: '剩 12 天', entries: 168, joined: false },
{ id: 6, theme: '反义词情书', prompt: '每行包含一组反义词', days: '剩 5 天', entries: 434, joined: false },
{ id: 7, theme: '地铁站名串联', prompt: '用 5 个真实站名写副歌', days: '剩 8 天', entries: 203, joined: false },
{ id: 8, theme: '十二行月亮', prompt: '月亮只能出现一次', days: '剩 15 天', entries: 512, joined: false }
];
const NOTE_CARDS: NoteCard[] = [
{ id: 1, text: '电梯里的香水味,是没说出口的再见', color: 'neon', date: '今天 07:42', pinned: true },
{ id: 2, text: '把"舍不得"拆成三个小节来唱', color: 'yellow', date: '昨天 23:18', pinned: true },
{ id: 3, text: '副歌押 ang 韵,桥段换成 ou 韵转冷', color: 'cyan', date: '昨天 21:03', pinned: false },
{ id: 4, text: '灵感:红绿灯坏了的十字路口,所有人都在等', color: 'neon', date: '08-24', pinned: false },
{ id: 5, text: '歌名备选:《加载中》《缓冲》《转圈的小圈》', color: 'yellow', date: '08-23', pinned: false },
{ id: 6, text: '第一句要用具体名词开场:硬币、站台、雨刮器', color: 'cyan', date: '08-22', pinned: false },
{ id: 7, text: '桥段留白四小节,只放一句气声', color: 'neon', date: '08-20', pinned: false },
{ id: 8, text: '写完别急着改,睡一觉再看', color: 'yellow', date: '08-18', pinned: false }
];
const INK_WEEK: InkDay[] = [
{ day: '一', lines: 6 },
{ day: '二', lines: 12 },
{ day: '三', lines: 4 },
{ day: '四', lines: 18 },
{ day: '五', lines: 9 },
{ day: '六', lines: 26 },
{ day: '日', lines: 15 }
];
const STYLE_STATS: StyleStat[] = [
{ name: '都市叙事', emoji: '🏙', percent: 34 },
{ name: '意象流', emoji: '🌫', percent: 26 },
{ name: '民谣', emoji: '🪕', percent: 21 },
{ name: '说唱', emoji: '🎤', percent: 19 }
];
const STYLE_FILTERS: string[] = ['全部', '都市叙事', '意象流', '民谣', '说唱', '校园', '童趣'];
// ============ 工具函数 ============
function noteBg(c: string): string {
if (c === 'yellow') {
return 'rgba(255,224,102,0.16)';
}
if (c === 'cyan') {
return 'rgba(102,232,255,0.12)';
}
return 'rgba(255,105,180,0.12)';
}
function noteBorder(c: string): string {
if (c === 'yellow') {
return 'rgba(255,224,102,0.5)';
}
if (c === 'cyan') {
return 'rgba(102,232,255,0.4)';
}
return 'rgba(255,105,180,0.5)';
}
function noteAccent(c: string): string {
if (c === 'yellow') {
return '#FFE066';
}
if (c === 'cyan') {
return '#66E8FF';
}
return '#FF69B4';
}
function workStatusColor(s: string): string {
if (s === '已发布') {
return '#66E8FF';
}
return '#9A8FC9';
}
function inkBar(h: number): number {
return h * 2.6;
}
enum LyricTab {
SQUARE,
RHYME,
CHALLENGE,
NOTES,
WORKS,
ME
}
// ============ 主入口 ============
@Entry
@Component
struct LyricraftApp {
@State activeTab: LyricTab = LyricTab.SQUARE
@Builder contentArea() {
if (this.activeTab === LyricTab.SQUARE) {
SquareContent()
} else if (this.activeTab === LyricTab.RHYME) {
RhymeContent()
} else if (this.activeTab === LyricTab.CHALLENGE) {
LyricChallengeContent()
} else if (this.activeTab === LyricTab.NOTES) {
NotesContent()
} else if (this.activeTab === LyricTab.WORKS) {
WorksContent()
} else {
MeLyricContent()
}
}
@Builder bottomTabItem(icon: string, label: string, tab: LyricTab) {
Column({ space: 3 }) {
Text(icon)
.fontSize(20)
.scale(this.activeTab === tab ? { x: 1.18, y: 1.18 } : { x: 1, y: 1 })
.animation({ duration: 220, curve: Curve.EaseOut })
Text(label)
.fontSize(10)
.fontColor(this.activeTab === tab ? '#FF69B4' : '#6F6795')
}
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 6 })
.width('16.6%')
.onClick(() => {
this.activeTab = tab;
})
}
build() {
Column() {
LyricraftHeader()
Column() {
this.contentArea()
}
.layoutWeight(1)
.width('100%')
Divider()
.color('#2E2749')
.strokeWidth(1)
Row() {
this.bottomTabItem('✒️', '广场', LyricTab.SQUARE)
this.bottomTabItem('🔤', '押韵', LyricTab.RHYME)
this.bottomTabItem('🎯', '挑战', LyricTab.CHALLENGE)
this.bottomTabItem('📌', '便签', LyricTab.NOTES)
this.bottomTabItem('📚', '作品', LyricTab.WORKS)
this.bottomTabItem('👤', '我的', LyricTab.ME)
}
.width('100%')
.backgroundColor('#1B1630')
.padding({ bottom: 6 })
}
.width('100%')
.height('100%')
.backgroundColor('#151026')
}
}
// ============ 头部(电商风·静态) ============
@Component
struct LyricraftHeader {
onSearchTap: () => void = () => {};
build() {
Column() {
Row({ space: 10 }) {
Column() {
Text('词')
.fontSize(19)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
}
.width(36)
.height(36)
.borderRadius(12)
.backgroundColor('#FF69B4')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('词格 LYRICRAFT')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('把心事押进韵脚里')
.fontSize(9)
.fontColor('#6F6795')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🔔')
.fontSize(19)
.padding(5)
Text('💬')
.fontSize(19)
.padding(5)
}
.width('100%')
.padding({ left: 16, right: 16, top: 10 })
Row({ space: 8 }) {
Text('🔍')
.fontSize(14)
Text('搜歌词 / 韵脚 / 写作灵感')
.fontSize(12)
.fontColor('#6F6795')
.layoutWeight(1)
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.borderRadius(20)
.backgroundColor('#221B3B')
.margin({ left: 16, right: 16, top: 10 })
.onClick(() => {
this.onSearchTap();
})
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('🖊')
.fontSize(22)
Text('每日一题')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
Text('今日主题:晚高峰')
.fontSize(9)
.fontColor('rgba(21,16,38,0.7)')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1.3)
.padding(14)
.borderRadius(14)
.backgroundColor('#FF69B4')
Column({ space: 4 }) {
Text('📖')
.fontSize(22)
Text('押韵词典')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#66E8FF')
Text('2000+ 韵脚')
.fontSize(9)
.fontColor('#6F6795')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.padding(14)
.borderRadius(14)
.backgroundColor('#221B3B')
}
.width('94%')
.margin({ top: 10, bottom: 12 })
}
.width('100%')
.backgroundColor('#1B1630')
}
}
// ============ 广场 Tab(作品瀑布流 + 点赞 + 举报弹窗) ============
@Component
struct SquareContent {
@State styleIdx: number = 0
@State showWorkDetail: boolean = false
@State selected: LyricWork | null = null
@State likedIds: number[] = [2, 6]
@State showLikeToast: boolean = false
@State showReport: boolean = false
worksNow(): LyricWork[] {
return LYRIC_WORKS.filter((w: LyricWork) => {
if (this.styleIdx === 0) {
return true;
}
return w.style === STYLE_FILTERS[this.styleIdx];
});
}
isLiked(w: LyricWork): boolean {
return this.likedIds.indexOf(w.id) >= 0;
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
onClose();
})
Column() {
Text('✒️')
.fontSize(30)
.margin({ bottom: 8 })
Text('词格广场')
.fontSize(12)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder workDetailModal() {
Column({ space: 12 }) {
Text('作 品 抽 读')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 20 })
Text(this.selected!.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text(this.selected!.author + ' · ' + this.selected!.style + ' · ' + this.selected!.lines.toString() + ' 行')
.fontSize(11)
.fontColor('#6F6795')
Column({ space: 8 }) {
Text('晚高峰的便利店,')
.fontSize(13)
.fontColor('#C9C1E4')
Text('关东煮替我留着最后一格温暖,')
.fontSize(13)
.fontColor('#C9C1E4')
Text('玻璃门开合六次,')
.fontSize(13)
.fontColor('#C9C1E4')
Text('没有人进来,风替谁看了一眼。')
.fontSize(13)
.fontColor('#C9C1E4')
}
.width('84%')
.padding(16)
.backgroundColor('#221B3B')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
Row({ space: 12 }) {
Text(this.isLiked(this.selected!) ? '♥ ' + (this.selected!.likes + 1).toString() : '♡ ' + this.selected!.likes.toString())
.fontSize(13)
.fontColor(this.isLiked(this.selected!) ? '#FF69B4' : '#9A8FC9')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(18)
.backgroundColor('rgba(255,105,180,0.12)')
.scale(this.isLiked(this.selected!) ? { x: 1.1, y: 1.1 } : { x: 1, y: 1 })
.animation({ duration: 240, curve: Curve.EaseOut })
.onClick(() => {
if (this.likedIds.indexOf(this.selected!.id) < 0) {
this.likedIds.push(this.selected!.id);
} else {
this.likedIds.splice(this.likedIds.indexOf(this.selected!.id), 1);
}
})
Text('⤴ 分享')
.fontSize(13)
.fontColor('#66E8FF')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(18)
.backgroundColor('rgba(102,232,255,0.1)')
.onClick(() => {
this.showWorkDetail = false;
this.showLikeToast = true;
})
Text('⚑ 举报')
.fontSize(13)
.fontColor('#9A8FC9')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(18)
.backgroundColor('rgba(154,143,201,0.12)')
.onClick(() => {
this.showReport = true;
})
}
.margin({ top: 4, bottom: 22 })
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder reportModal() {
Column({ space: 12 }) {
Text('⚑')
.fontSize(30)
.margin({ top: 22 })
Text('举报该作品?')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('洗稿 / 抄袭 / 不良内容')
.fontSize(11)
.fontColor('#6F6795')
Column({ space: 8 }) {
ForEach(['疑似抄袭洗稿', '内容低质灌水', '不良信息'], (r: string) => {
Text(r)
.fontSize(12)
.fontColor('#C9C1E4')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#221B3B')
.margin({ top: 4 })
}, (r: string) => r)
}
Row({ space: 12 }) {
Button() {
Text('取消')
.fontSize(13)
.fontColor('#6F6795')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showReport = false;
})
Button() {
Text('提交举报')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#FF69B4')
.borderRadius(20)
.onClick(() => {
this.showReport = false;
this.showWorkDetail = false;
})
}
.width('80%')
.margin({ top: 6, bottom: 22 })
}
.width('80%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
build() {
Stack() {
Scroll() {
Column() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(STYLE_FILTERS, (s: string, i: number) => {
Text(s)
.fontSize(12)
.fontColor(this.styleIdx === i ? '#151026' : '#9A8FC9')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(18)
.backgroundColor(this.styleIdx === i ? '#FF69B4' : '#221B3B')
.margin({ right: 8, bottom: 8 })
.scale(this.styleIdx === i ? { x: 1.05, y: 1.05 } : { x: 1, y: 1 })
.animation({ duration: 200, curve: Curve.EaseOut })
.onClick(() => {
this.styleIdx = i;
})
}, (s: string) => s)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.worksNow(), (w: LyricWork) => {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text(w.author.slice(0, 1))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
}
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('#66E8FF')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text(w.author)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text(w.style + ' · ' + w.lines.toString() + ' 行')
.fontSize(9)
.fontColor('#6F6795')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(w.status)
.fontSize(9)
.fontColor(workStatusColor(w.status))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(3)
.backgroundColor('rgba(102,232,255,0.08)')
}
.width('100%')
Column({ space: 6 }) {
Text('「 晚高峰的便利店,关东煮替我留着最后一格温暖 」')
.fontSize(12)
.fontColor('#C9C1E4')
.lineHeight(19)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
Text('—— ' + w.title)
.fontSize(10)
.fontColor('#FF69B4')
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor('#221B3B')
.borderRadius(10)
.alignItems(HorizontalAlign.Start)
Row({ space: 12 }) {
Text(this.isLiked(w) ? '♥ ' + (w.likes + 1).toString() : '♡ ' + w.likes.toString())
.fontSize(11)
.fontColor(this.isLiked(w) ? '#FF69B4' : '#6F6795')
.scale(this.isLiked(w) ? { x: 1.12, y: 1.12 } : { x: 1, y: 1 })
.animation({ duration: 240, curve: Curve.EaseOut })
.onClick(() => {
if (this.likedIds.indexOf(w.id) < 0) {
this.likedIds.push(w.id);
this.showLikeToast = true;
}
})
Text('💬 评论')
.fontSize(11)
.fontColor('#6F6795')
Text('⤴')
.fontSize(11)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.padding(14)
.backgroundColor('#1B1630')
.borderRadius(14)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 8 })
.onClick(() => {
this.selected = w;
this.showWorkDetail = true;
})
}, (w: LyricWork) => w.id.toString())
Text('— 今日已更 14 首,笔耕不辍 —')
.fontSize(10)
.fontColor('#4E4670')
.margin({ top: 18, bottom: 24 })
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
if (this.showWorkDetail) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showWorkDetail = false;
})
this.workDetailModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
}
if (this.showReport) {
Column() {
this.reportModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.75)')
.justifyContent(FlexAlign.Center)
}
if (this.showLikeToast) {
Column() {
Text('♥ 已点赞,好句值得被看见')
.fontSize(13)
.fontColor('#151026')
.padding({ left: 22, right: 22, top: 11, bottom: 11 })
.borderRadius(22)
.backgroundColor('rgba(255,105,180,0.95)')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 240, curve: Curve.EaseOut })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showLikeToast = false;
})
}
}
.width('100%')
.height('100%')
}
}
// ============ 押韵 Tab(韵脚分行 + 词条弹窗 + 收藏) ============
@Component
struct RhymeContent {
@State showRhymeDetail: boolean = false
@State selected: RhymeGroup | null = null
@State showFavToast: boolean = false
@State favVowels: number[] = [1, 3]
isFav(r: RhymeGroup): boolean {
return this.favVowels.indexOf(r.id) >= 0;
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
onClose();
})
Column() {
Text('🔤')
.fontSize(30)
.margin({ bottom: 8 })
Text('押韵词典')
.fontSize(12)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder rhymeDetailModal() {
Column({ space: 12 }) {
Text('韵 脚 详 情')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 20 })
Text(this.selected!.vowel)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#66E8FF')
Text('常用示例:' + this.selected!.sample)
.fontSize(11)
.fontColor('#6F6795')
Divider()
.color('#2E2749')
.strokeWidth(1)
.width('84%')
Column({ space: 10 }) {
ForEach(['晚风把街道折成信纸 / 光', '人群散场后的十字 / 巷', '谁在楼下轻声 / 唱', '路灯替谁把眼睛擦 / 亮'], (l: string) => {
Text('「 ' + l + ' 」')
.fontSize(12)
.fontColor('#C9C1E4')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#221B3B')
.width('88%')
.textAlign(TextAlign.Center)
}, (l: string) => l)
}
.width('100%')
Row({ space: 10 }) {
Button() {
Text(this.isFav(this.selected!) ? '★ 已收藏韵脚' : '☆ 收藏此韵')
.fontSize(13)
.fontColor(this.isFav(this.selected!) ? '#151026' : '#66E8FF')
}
.layoutWeight(1)
.height(42)
.backgroundColor(this.isFav(this.selected!) ? '#66E8FF' : 'rgba(102,232,255,0.12)')
.borderRadius(21)
.onClick(() => {
if (this.favVowels.indexOf(this.selected!.id) < 0) {
this.favVowels.push(this.selected!.id);
}
this.showRhymeDetail = false;
this.showFavToast = true;
})
Button() {
Text('✎ 用此韵开写')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(42)
.backgroundColor('#FF69B4')
.borderRadius(21)
.onClick(() => {
this.showRhymeDetail = false;
this.showFavToast = true;
})
}
.width('84%')
.margin({ top: 6, bottom: 22 })
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
build() {
Stack() {
Scroll() {
Column() {
Column({ space: 6 }) {
Text('🔤 输入尾字,秒查同韵词')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
Text('如输入「光」→ ang 韵 286 词')
.fontSize(10)
.fontColor('rgba(21,16,38,0.7)')
}
.width('94%')
.padding(16)
.backgroundColor('#66E8FF')
.borderRadius(14)
.margin({ top: 12 })
Row() {
Text('十 大 高 频 韵 脚')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
.letterSpacing(2)
.layoutWeight(1)
Text('已收藏 ' + this.favVowels.length.toString() + ' 韵')
.fontSize(10)
.fontColor('#6F6795')
}
.width('94%')
.margin({ top: 14, bottom: 4 })
ForEach(RHYME_GROUPS, (r: RhymeGroup) => {
Row({ space: 12 }) {
Column() {
Text(r.vowel.slice(0, 1))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
}
.width(44)
.height(44)
.borderRadius(12)
.backgroundColor('#FFE066')
.justifyContent(FlexAlign.Center)
Column({ space: 5 }) {
Text(r.vowel)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text(r.sample)
.fontSize(10)
.fontColor('#6F6795')
Text(r.words)
.fontSize(9)
.fontColor('#FF69B4')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Column()
.width((r.count / 3.2).toFixed(0) + 'vp')
.height(5)
.borderRadius(3)
.backgroundColor('#66E8FF')
}
.width(96)
.height(5)
.borderRadius(3)
.backgroundColor('#221B3B')
Text(this.isFav(r) ? '★' : '☆')
.fontSize(16)
.fontColor(this.isFav(r) ? '#FFE066' : '#6F6795')
.padding(6)
.onClick(() => {
if (this.favVowels.indexOf(r.id) < 0) {
this.favVowels.push(r.id);
} else {
this.favVowels.splice(this.favVowels.indexOf(r.id), 1);
}
})
}
.width('100%')
.padding(14)
.backgroundColor('#1B1630')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 8 })
.onClick(() => {
this.selected = r;
this.showRhymeDetail = true;
})
}, (r: RhymeGroup) => r.id.toString())
Text('— 韵脚对了,歌就顺了 —')
.fontSize(10)
.fontColor('#4E4670')
.margin({ top: 18, bottom: 24 })
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
if (this.showRhymeDetail) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showRhymeDetail = false;
})
this.rhymeDetailModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
}
if (this.showFavToast) {
Column() {
Text('✓ 已加入韵脚收藏本')
.fontSize(13)
.fontColor('#151026')
.padding({ left: 22, right: 22, top: 11, bottom: 11 })
.borderRadius(22)
.backgroundColor('rgba(102,232,255,0.95)')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showFavToast = false;
})
}
}
.width('100%')
.height('100%')
}
}
// ============ 挑战 Tab(填词挑战 + 参赛投稿弹窗) ============
@Component
struct LyricChallengeContent {
@State showDetail: boolean = false
@State selected: ChallengePost | null = null
@State showSubmitModal: boolean = false
@State submitText: string = ''
@State showSubmitToast: boolean = false
@State joinedIds: number[] = [1]
isJoined(c: ChallengePost): boolean {
return this.joinedIds.indexOf(c.id) >= 0;
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
onClose();
})
Column() {
Text('🎯')
.fontSize(30)
.margin({ bottom: 8 })
Text('填词挑战')
.fontSize(12)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder detailModal() {
Column({ space: 12 }) {
Text('挑 战 详 情')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 20 })
Text(this.selected!.theme)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('命题:' + this.selected!.prompt)
.fontSize(11)
.fontColor('#66E8FF')
Divider()
.color('#2E2749')
.strokeWidth(1)
.width('84%')
Column({ space: 10 }) {
Row() {
Text('已有投稿')
.fontSize(11)
.fontColor('#6F6795')
Text(this.selected!.entries.toString() + ' 份')
.fontSize(11)
.fontColor('#EDE9F7')
}
.width('84%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('截止')
.fontSize(11)
.fontColor('#6F6795')
Text(this.selected!.days)
.fontSize(11)
.fontColor('#FF69B4')
}
.width('84%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('评审方式')
.fontSize(11)
.fontColor('#6F6795')
Text('点赞数前 3 进编辑终审')
.fontSize(11)
.fontColor('#EDE9F7')
}
.width('84%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
Column({ space: 8 }) {
Text('热评投稿')
.fontSize(11)
.fontColor('#6F6795')
.width('88%')
ForEach(['「打印机停下的那一秒,办公室突然有了心跳」', '「格子间的日光灯,比月亮更懂加班」', '「便利店的门铃,是深夜唯一的掌声」'], (l: string) => {
Text(l)
.fontSize(11)
.fontColor('#C9C1E4')
.padding({ left: 12, right: 12, top: 9, bottom: 9 })
.borderRadius(10)
.backgroundColor('#221B3B')
.width('88%')
.lineHeight(16)
}, (l: string) => l)
}
.width('100%')
Button() {
Text(this.isJoined(this.selected!) ? '✓ 已投稿 · 查看我的' : '✎ 投稿参赛')
.fontSize(14)
.fontColor('#151026')
}
.width('70%')
.height(42)
.backgroundColor(this.isJoined(this.selected!) ? '#4E4670' : '#FF69B4')
.borderRadius(21)
.margin({ top: 4, bottom: 22 })
.onClick(() => {
this.showDetail = false;
if (!this.isJoined(this.selected!)) {
this.showSubmitModal = true;
}
})
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
.constraintSize({ maxHeight: '80%' })
}
@Builder submitModal() {
Column({ space: 12 }) {
Text('投 稿')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 22 })
Text('主题:' + this.selected!.theme)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
TextArea({ placeholder: '写下你的句子(限 60 字)…' })
.fontSize(13)
.fontColor('#EDE9F7')
.width('84%')
.height(110)
.backgroundColor('#221B3B')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.onChange((v: string) => {
this.submitText = v;
})
Text('小技巧:第一句落具体名词,第二句押韵收尾')
.fontSize(10)
.fontColor('#6F6795')
Row({ space: 12 }) {
Button() {
Text('再想想')
.fontSize(13)
.fontColor('#6F6795')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showSubmitModal = false;
})
Button() {
Text('提交投稿')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#FF69B4')
.borderRadius(20)
.onClick(() => {
this.submitText = '';
this.joinedIds.push(this.selected!.id);
this.showSubmitModal = false;
this.showSubmitToast = true;
})
}
.width('84%')
.margin({ top: 6, bottom: 22 })
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
build() {
Stack() {
Scroll() {
Column() {
Row({ space: 12 }) {
Column({ space: 5 }) {
Text('🏆')
.fontSize(24)
Text('本月词王赛')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
Text('投稿即得限定徽章')
.fontSize(9)
.fontColor('rgba(21,16,38,0.7)')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1.4)
.padding(16)
.backgroundColor('#FFE066')
.borderRadius(14)
Column({ space: 5 }) {
Text('📜')
.fontSize(24)
Text('我的战绩')
.fontSize(11)
.fontColor('#EDE9F7')
Text('投稿 6 · 获奖 1')
.fontSize(9)
.fontColor('#66E8FF')
}
.alignItems(HorizontalAlign.Start)
.padding(16)
.backgroundColor('#221B3B')
.borderRadius(14)
}
.width('94%')
.margin({ top: 12 })
Row() {
Text('进 行 中 的 命 题')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
.letterSpacing(2)
.layoutWeight(1)
Text('8 个题目')
.fontSize(10)
.fontColor('#6F6795')
}
.width('94%')
.margin({ top: 14, bottom: 4 })
ForEach(CHALLENGE_POSTS, (c: ChallengePost) => {
Column({ space: 10 }) {
Row({ space: 12 }) {
Column() {
Text(c.theme.slice(0, 1))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
}
.width(44)
.height(44)
.borderRadius(12)
.backgroundColor('#FF69B4')
.justifyContent(FlexAlign.Center)
Column({ space: 5 }) {
Row({ space: 6 }) {
Text(c.theme)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.isJoined(c) ? '已投稿' : '可投稿')
.fontSize(9)
.fontColor(this.isJoined(c) ? '#66E8FF' : '#FF69B4')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(3)
.backgroundColor(this.isJoined(c) ? 'rgba(102,232,255,0.1)' : 'rgba(255,105,180,0.12)')
}
.width('100%')
Text('命题:' + c.prompt)
.fontSize(10)
.fontColor('#6F6795')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(c.days + ' · ' + c.entries.toString() + ' 份投稿')
.fontSize(9)
.fontColor('#4E4670')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Column() {
Column()
.width(Math.min(c.entries / 9, 100).toFixed(0) + '%')
.height(5)
.borderRadius(3)
.backgroundColor('#66E8FF')
.animation({ duration: 300, curve: Curve.EaseOut })
}
.width('100%')
.height(5)
.borderRadius(3)
.backgroundColor('#221B3B')
}
.width('100%')
.padding(14)
.backgroundColor('#1B1630')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 8 })
.onClick(() => {
this.selected = c;
this.showDetail = true;
})
}, (c: ChallengePost) => c.id.toString())
Text('— 好句子都在命题里发芽 —')
.fontSize(10)
.fontColor('#4E4670')
.margin({ top: 18, bottom: 24 })
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
if (this.showDetail) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showDetail = false;
})
this.detailModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
}
if (this.showSubmitModal) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showSubmitModal = false;
})
this.submitModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.75)')
}
if (this.showSubmitToast) {
Column() {
Text('✓ 投稿成功,等待同好点亮')
.fontSize(13)
.fontColor('#151026')
.padding({ left: 22, right: 22, top: 11, bottom: 11 })
.borderRadius(22)
.backgroundColor('rgba(255,224,102,0.95)')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 240, curve: Curve.EaseOut })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showSubmitToast = false;
})
}
}
.width('100%')
.height('100%')
}
}
// ============ 便签 Tab(灵感便签 + 新建/编辑/删除弹窗) ============
@Component
struct NotesContent {
@State notes: NoteCard[] = NOTE_CARDS
@State showNewNote: boolean = false
@State showEditNote: boolean = false
@State showDeleteNote: boolean = false
@State showPinToast: boolean = false
@State noteText: string = ''
@State editingId: number = 0
@State deletingId: number = 0
findNote(id: number): NoteCard {
return this.notes.filter((n: NoteCard) => n.id === id)[0];
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
onClose();
})
Column() {
Text('📌')
.fontSize(30)
.margin({ bottom: 8 })
Text('灵感便签')
.fontSize(12)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder newNoteModal() {
Column({ space: 12 }) {
Text('新 便 签')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 22 })
TextArea({ placeholder: '灵感、韵脚、歌名备选…(限 50 字)' })
.fontSize(13)
.fontColor('#EDE9F7')
.width('84%')
.height(100)
.backgroundColor('#221B3B')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.onChange((v: string) => {
this.noteText = v;
})
Text('颜色将随机分配:霓虹粉 / 荧光黄 / 青蓝')
.fontSize(10)
.fontColor('#6F6795')
Row({ space: 12 }) {
Button() {
Text('取消')
.fontSize(13)
.fontColor('#6F6795')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showNewNote = false;
})
Button() {
Text('贴上墙')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#FFE066')
.borderRadius(20)
.onClick(() => {
this.notes.unshift({
id: this.notes.length + 1,
text: this.noteText.length > 0 ? this.noteText : '一条新的灵感',
color: 'neon',
date: '刚刚',
pinned: false
});
this.noteText = '';
this.showNewNote = false;
})
}
.width('84%')
.margin({ top: 6, bottom: 22 })
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder editNoteModal() {
Column({ space: 12 }) {
Text('编 辑 便 签')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 22 })
Text('当前内容:')
.fontSize(11)
.fontColor('#6F6795')
.width('84%')
Text('「' + this.findNote(this.editingId).text + '」')
.fontSize(12)
.fontColor('#C9C1E4')
.width('84%')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
TextArea({ placeholder: '修改便签内容…' })
.fontSize(13)
.fontColor('#EDE9F7')
.width('84%')
.height(90)
.backgroundColor('#221B3B')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.onChange((v: string) => {
this.noteText = v;
})
Row({ space: 12 }) {
Button() {
Text('取消')
.fontSize(13)
.fontColor('#6F6795')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showEditNote = false;
})
Button() {
Text('保存')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#66E8FF')
.borderRadius(20)
.onClick(() => {
this.findNote(this.editingId).text = this.noteText.length > 0 ? this.noteText : this.findNote(this.editingId).text;
this.noteText = '';
this.showEditNote = false;
})
}
.width('84%')
.margin({ top: 6, bottom: 22 })
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder deleteNoteModal() {
Column({ space: 12 }) {
Text('⚠')
.fontSize(34)
.margin({ top: 22 })
Text('撕掉这张便签?')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('「' + this.findNote(this.deletingId).text + '」')
.fontSize(11)
.fontColor('#6F6795')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('70%')
.textAlign(TextAlign.Center)
Text('灵感一旦撕掉,就真的没了')
.fontSize(10)
.fontColor('#4E4670')
Row({ space: 12 }) {
Button() {
Text('留着')
.fontSize(13)
.fontColor('#66E8FF')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showDeleteNote = false;
})
Button() {
Text('撕掉')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#FF69B4')
.borderRadius(20)
.onClick(() => {
this.notes.splice(this.notes.map((n: NoteCard) => n.id).indexOf(this.deletingId), 1);
this.showDeleteNote = false;
})
}
.width('80%')
.margin({ top: 4, bottom: 22 })
}
.width('80%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
build() {
Stack() {
Scroll() {
Column() {
Row() {
Column({ space: 4 }) {
Text('📌 灵感墙')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('置顶 ' + this.notes.filter((n: NoteCard) => n.pinned).length.toString() + ' 条 · 共 ' + this.notes.length.toString() + ' 条')
.fontSize(10)
.fontColor('#6F6795')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('+ 新便签')
.fontSize(11)
.fontColor('#151026')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FFE066')
.onClick(() => {
this.showNewNote = true;
})
}
.width('94%')
.margin({ top: 12 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.notes, (n: NoteCard) => {
Column({ space: 8 }) {
Row() {
Text(n.pinned ? '📌' : '')
.fontSize(12)
.layoutWeight(1)
Text('✎')
.fontSize(13)
.fontColor(noteAccent(n.color))
.padding(4)
.onClick(() => {
this.editingId = n.id;
this.noteText = '';
this.showEditNote = true;
})
Text('🗑')
.fontSize(12)
.fontColor('#9A8FC9')
.padding(4)
.onClick(() => {
this.deletingId = n.id;
this.showDeleteNote = true;
})
}
.width('100%')
Text(n.text)
.fontSize(12)
.fontColor('#EDE9F7')
.lineHeight(18)
.width('100%')
Text(n.date)
.fontSize(9)
.fontColor('#6F6795')
.width('100%')
}
.width('46%')
.margin(6)
.padding(12)
.backgroundColor(noteBg(n.color))
.borderRadius(6)
.border({ width: 1, color: noteBorder(n.color) })
.rotate({ angle: n.id % 3 === 0 ? -1.5 : (n.id % 3 === 1 ? 1 : 0.8) })
.alignItems(HorizontalAlign.Start)
}, (n: NoteCard) => n.id.toString())
}
.width('100%')
.padding({ left: 10, right: 10, top: 10 })
Text('— 灵感稍纵即逝,先钉在墙上 —')
.fontSize(10)
.fontColor('#4E4670')
.margin({ top: 14, bottom: 24 })
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
if (this.showNewNote) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showNewNote = false;
})
this.newNoteModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
}
if (this.showEditNote) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showEditNote = false;
})
this.editNoteModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
}
if (this.showDeleteNote) {
Column() {
this.deleteNoteModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.75)')
.justifyContent(FlexAlign.Center)
}
if (this.showPinToast) {
Column() {
Text('✓ 已置顶到灵感墙最上方')
.fontSize(13)
.fontColor('#151026')
.padding({ left: 22, right: 22, top: 11, bottom: 11 })
.borderRadius(22)
.backgroundColor('rgba(255,224,102,0.95)')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showPinToast = false;
})
}
}
.width('100%')
.height('100%')
}
}
// ============ 作品 Tab(我的词稿 + 编辑/删除/发布弹窗) ============
@Component
struct WorksContent {
@State statusIdx: number = 0
@State showEdit: boolean = false
@State showDelete: boolean = false
@State showPublish: boolean = false
@State showPublishToast: boolean = false
@State selected: LyricWork | null = null
@State editTitle: string = ''
statusFilters(): string[] {
return ['全部', '草稿', '已发布'];
}
worksNow(): LyricWork[] {
return LYRIC_WORKS.filter((w: LyricWork) => w.author === '速写本').filter((w: LyricWork) => {
if (this.statusIdx === 0) {
return true;
}
return w.status === this.statusFilters()[this.statusIdx];
});
}
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
onClose();
})
Column() {
Text('📚')
.fontSize(30)
.margin({ bottom: 8 })
Text('我的词稿')
.fontSize(12)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder editModal() {
Column({ space: 12 }) {
Text('改 词 稿 名')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 22 })
Text('当前:《' + this.selected!.title + '》')
.fontSize(11)
.fontColor('#6F6795')
TextInput({ placeholder: '输入新的词稿名' })
.fontSize(13)
.fontColor('#EDE9F7')
.width('84%')
.height(44)
.backgroundColor('#221B3B')
.borderRadius(10)
.border({ width: 1, color: '#2E2749' })
.onChange((v: string) => {
this.editTitle = v;
})
Row({ space: 12 }) {
Button() {
Text('取消')
.fontSize(13)
.fontColor('#6F6795')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showEdit = false;
})
Button() {
Text('保存')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#66E8FF')
.borderRadius(20)
.onClick(() => {
this.selected!.title = this.editTitle.length > 0 ? this.editTitle : this.selected!.title;
this.editTitle = '';
this.showEdit = false;
})
}
.width('84%')
.margin({ top: 6, bottom: 22 })
}
.width('88%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder deleteModal() {
Column({ space: 12 }) {
Text('⚠')
.fontSize(34)
.margin({ top: 22 })
Text('删除这份词稿?')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('《' + this.selected!.title + '》· ' + this.selected!.lines.toString() + ' 行')
.fontSize(11)
.fontColor('#6F6795')
Text('删除后可从回收站找回 30 天')
.fontSize(10)
.fontColor('#4E4670')
Row({ space: 12 }) {
Button() {
Text('保留')
.fontSize(13)
.fontColor('#66E8FF')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showDelete = false;
})
Button() {
Text('删除词稿')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#FF69B4')
.borderRadius(20)
.onClick(() => {
this.showDelete = false;
})
}
.width('84%')
.margin({ top: 4, bottom: 22 })
}
.width('80%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder publishModal() {
Column({ space: 12 }) {
Text('发 布 词 稿')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 22 })
Text('《' + this.selected!.title + '》')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Column({ space: 8 }) {
Row() {
Text('作品风格')
.fontSize(11)
.fontColor('#6F6795')
Text(this.selected!.style)
.fontSize(11)
.fontColor('#66E8FF')
}
.width('84%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('总行数')
.fontSize(11)
.fontColor('#6F6795')
Text(this.selected!.lines.toString() + ' 行')
.fontSize(11)
.fontColor('#EDE9F7')
}
.width('84%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('发布范围')
.fontSize(11)
.fontColor('#6F6795')
Text('广场公开(可被合作邀约)')
.fontSize(11)
.fontColor('#EDE9F7')
}
.width('84%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
Row({ space: 12 }) {
Button() {
Text('再改改')
.fontSize(13)
.fontColor('#6F6795')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showPublish = false;
})
Button() {
Text('确认发布')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#FFE066')
.borderRadius(20)
.onClick(() => {
this.showPublish = false;
this.showPublishToast = true;
})
}
.width('84%')
.margin({ top: 6, bottom: 22 })
}
.width('84%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
build() {
Stack() {
Scroll() {
Column() {
Row() {
ForEach(this.statusFilters(), (s: string, i: number) => {
Column({ space: 4 }) {
Text(s)
.fontSize(12)
.fontColor(this.statusIdx === i ? '#FF69B4' : '#6F6795')
Divider()
.color(this.statusIdx === i ? '#FF69B4' : 'rgba(0,0,0,0)')
.strokeWidth(2)
.width('50%')
}
.padding({ left: 16, right: 16, top: 10, bottom: 6 })
.onClick(() => {
this.statusIdx = i;
})
}, (s: string) => s)
}
.width('100%')
.backgroundColor('#1B1630')
.justifyContent(FlexAlign.SpaceEvenly)
Row() {
Text('共 ' + this.worksNow().length.toString() + ' 份词稿')
.fontSize(10)
.fontColor('#6F6795')
Text('✔ 已开启云端同步')
.fontSize(10)
.fontColor('#66E8FF')
}
.width('94%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 10, bottom: 4 })
ForEach(this.worksNow(), (w: LyricWork) => {
Row({ space: 12 }) {
Column() {
Text(w.status === '草稿' ? '✎' : '✓')
.fontSize(16)
.fontColor(w.status === '草稿' ? '#FFE066' : '#66E8FF')
}
.width(44)
.height(44)
.borderRadius(12)
.backgroundColor('rgba(255,224,102,0.1)')
.justifyContent(FlexAlign.Center)
Column({ space: 5 }) {
Text('《' + w.title + '》')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(w.style + ' · ' + w.lines.toString() + ' 行 · ♡ ' + w.likes.toString())
.fontSize(10)
.fontColor('#6F6795')
Text(w.status === '草稿' ? '最后编辑于昨天' : '发布于 3 天前')
.fontSize(9)
.fontColor('#4E4670')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Text('✎')
.fontSize(14)
.fontColor('#66E8FF')
.padding(4)
.onClick(() => {
this.selected = w;
this.showEdit = true;
})
Text(w.status === '草稿' ? '🚀' : '🗑')
.fontSize(13)
.padding(4)
.onClick(() => {
this.selected = w;
if (w.status === '草稿') {
this.showPublish = true;
} else {
this.showDelete = true;
}
})
}
}
.width('100%')
.padding(14)
.backgroundColor('#1B1630')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 8 })
}, (w: LyricWork) => w.id.toString())
Text('— 词稿是写给未来自己的信 —')
.fontSize(10)
.fontColor('#4E4670')
.margin({ top: 18, bottom: 24 })
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
if (this.showEdit) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
this.showEdit = false;
})
this.editModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
}
if (this.showDelete) {
Column() {
this.deleteModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.75)')
.justifyContent(FlexAlign.Center)
}
if (this.showPublish) {
Column() {
this.publishModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
.justifyContent(FlexAlign.Center)
}
if (this.showPublishToast) {
Column() {
Text('✓ 已发布到广场,等待第一个读者')
.fontSize(13)
.fontColor('#151026')
.padding({ left: 22, right: 22, top: 11, bottom: 11 })
.borderRadius(22)
.backgroundColor('rgba(255,224,102,0.95)')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 240, curve: Curve.EaseOut })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showPublishToast = false;
})
}
}
.width('100%')
.height('100%')
}
}
// ============ 我的 Tab(码字图表 + 风格占比 + 合作者评价弹窗) ============
@Component
struct MeLyricContent {
@State showAbout: boolean = false
@State showLogout: boolean = false
@State showCoop: boolean = false
@Builder modalOverlay(onClose: () => void) {
Column() {
Column()
.width('100%')
.layoutWeight(1)
.onClick(() => {
onClose();
})
Column() {
Text('👤')
.fontSize(30)
.margin({ bottom: 8 })
Text('词人主页')
.fontSize(12)
.fontColor('#6F6795')
}
.width('100%')
}
.width('100%')
.height('100%')
}
@Builder coopModal() {
Column({ space: 12 }) {
Text('合 作 邀 约')
.fontSize(12)
.fontColor('#FF69B4')
.letterSpacing(3)
.margin({ top: 20 })
Text('来自作曲人的邀约')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Column({ space: 8 }) {
ForEach(['作曲·雾岛:想为《晚高峰的便利店》谱曲', '编曲·LAM:意象流 demo 求词', '歌手·陈迟:翻唱合作授权'], (c: string) => {
Text(c)
.fontSize(11)
.fontColor('#C9C1E4')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#221B3B')
.width('88%')
.lineHeight(16)
}, (c: string) => c)
}
.width('100%')
Button() {
Text('查看全部 3 条邀约')
.fontSize(13)
.fontColor('#151026')
}
.width('60%')
.height(40)
.backgroundColor('#66E8FF')
.borderRadius(20)
.margin({ top: 4, bottom: 22 })
.onClick(() => {
this.showCoop = false;
})
}
.width('86%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder aboutModal() {
Column({ space: 10 }) {
Text('词 格')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF69B4')
.letterSpacing(4)
.margin({ top: 24 })
Text('v1.9 · 立秋版')
.fontSize(11)
.fontColor('#6F6795')
Divider()
.color('#2E2749')
.strokeWidth(1)
.width('70%')
Text('每个韵脚都是一次心跳,')
.fontSize(12)
.fontColor('#9A8FC9')
Text('每行词都在等人谱曲。')
.fontSize(12)
.fontColor('#9A8FC9')
Text('© 2026 LYRICRAFT 词格')
.fontSize(10)
.fontColor('#4E4670')
.margin({ top: 8, bottom: 24 })
}
.width('78%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
@Builder logoutModal() {
Column({ space: 12 }) {
Text('👋')
.fontSize(34)
.margin({ top: 22 })
Text('确定退出登录?')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('未同步草稿将保留在本机')
.fontSize(10)
.fontColor('#6F6795')
Row({ space: 12 }) {
Button() {
Text('再写一会')
.fontSize(13)
.fontColor('#66E8FF')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#221B3B')
.borderRadius(20)
.onClick(() => {
this.showLogout = false;
})
Button() {
Text('退出')
.fontSize(13)
.fontColor('#151026')
}
.layoutWeight(1)
.height(40)
.backgroundColor('#4E4670')
.borderRadius(20)
.onClick(() => {
this.showLogout = false;
})
}
.width('80%')
.margin({ top: 4, bottom: 22 })
}
.width('78%')
.backgroundColor('#1B1630')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.border({ width: 1, color: '#2E2749' })
}
build() {
Stack() {
Scroll() {
Column() {
Row({ space: 14 }) {
Column() {
Text('速')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#151026')
}
.width(58)
.height(58)
.borderRadius(29)
.backgroundColor('#FFE066')
.justifyContent(FlexAlign.Center)
Column({ space: 5 }) {
Text('速写本')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
Text('词龄 2 年 · 累计 142 首')
.fontSize(11)
.fontColor('#6F6795')
Text('徽章:热门词作者 · 周更达人')
.fontSize(10)
.fontColor('#FF69B4')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(16)
.backgroundColor('#1B1630')
.borderRadius(14)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 12 })
Column({ space: 10 }) {
Row() {
Text('本 周 码 字 行 数')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
.letterSpacing(2)
.layoutWeight(1)
Text('共 90 行')
.fontSize(10)
.fontColor('#FF69B4')
}
.width('100%')
Row() {
ForEach(INK_WEEK, (d: InkDay, i: number) => {
Column({ space: 5 }) {
Column()
.width(16)
.height(inkBar(d.lines))
.borderRadius(4)
.backgroundColor(i >= 5 ? '#FF69B4' : '#66E8FF')
Text(d.day)
.fontSize(9)
.fontColor('#6F6795')
}
.alignItems(HorizontalAlign.Center)
}, (d: InkDay) => d.day)
}
.width('100%')
.height(86)
.alignItems(VerticalAlign.Bottom)
.justifyContent(FlexAlign.SpaceBetween)
Text('周六灵感爆发,单日 26 行')
.fontSize(10)
.fontColor('#6F6795')
}
.width('100%')
.padding(16)
.backgroundColor('#1B1630')
.borderRadius(14)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 10 })
Column({ space: 12 }) {
Text('作 品 风 格 占 比')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#EDE9F7')
.letterSpacing(2)
.width('100%')
ForEach(STYLE_STATS, (s: StyleStat) => {
Column({ space: 5 }) {
Row() {
Text(s.emoji + ' ' + s.name)
.fontSize(11)
.fontColor('#EDE9F7')
.layoutWeight(1)
Text(s.percent.toString() + '%')
.fontSize(11)
.fontColor('#FF69B4')
}
.width('100%')
Column() {
Column()
.width(s.percent + '%')
.height(6)
.borderRadius(3)
.backgroundColor(s.percent >= 25 ? '#FF69B4' : '#66E8FF')
.animation({ duration: 400, curve: Curve.EaseOut })
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#221B3B')
}
.width('100%')
}, (s: StyleStat) => s.name)
}
.width('100%')
.padding(16)
.backgroundColor('#1B1630')
.borderRadius(14)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 10 })
Row({ space: 12 }) {
Text('🤝 合作邀约')
.fontSize(13)
.fontColor('#66E8FF')
.layoutWeight(1)
Text('3 条新邀约')
.fontSize(10)
.fontColor('#FF69B4')
Text('›')
.fontSize(15)
.fontColor('#4E4670')
}
.width('100%')
.padding(16)
.backgroundColor('#1B1630')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 12 })
.onClick(() => {
this.showCoop = true;
})
Row({ space: 12 }) {
Text('ℹ 关于词格')
.fontSize(13)
.fontColor('#9A8FC9')
.layoutWeight(1)
Text('›')
.fontSize(15)
.fontColor('#4E4670')
}
.width('100%')
.padding(16)
.backgroundColor('#1B1630')
.borderRadius(12)
.border({ width: 1, color: '#2E2749' })
.margin({ left: 16, right: 16, top: 10 })
.onClick(() => {
this.showAbout = true;
})
Text('退出登录')
.fontSize(13)
.fontColor('#FF69B4')
.padding({ left: 32, right: 32, top: 12, bottom: 12 })
.borderRadius(22)
.backgroundColor('#1B1630')
.border({ width: 1, color: '#2E2749' })
.margin({ top: 14, bottom: 28 })
.onClick(() => {
this.showLogout = true;
})
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
if (this.showCoop) {
Column() {
this.coopModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
.justifyContent(FlexAlign.Center)
}
if (this.showAbout) {
Column() {
this.aboutModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
.justifyContent(FlexAlign.Center)
}
if (this.showLogout) {
Column() {
this.logoutModal()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(10,7,22,0.7)')
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
}
更多推荐



所有评论(0)