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

词格LYRICRAFT广场Tab

引言:把心事押进韵脚里

在音乐创作的链条中,歌词往往是最私人也最艰难的一环。作曲可以靠灵感,编曲可以靠技术,但歌词需要把无法言说的情绪拆解成一行行有韵脚的文字。多少人在深夜的备忘录里写下半句歌词,又因为找不到合适的韵脚、想不出好的主题、缺乏同好的反馈而搁置在草稿箱里。

词格 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 色彩层级与对比度

深色主题通过三级背景深度和四级文字明度构建清晰的视觉层级:

背景深度三级:

  1. 深紫底 #151026——页面主背景,最深最暗
  2. 深紫卡片 #1B1630——卡片、弹窗、列表项,比背景略亮形成层次
  3. 更深紫 #221B3B——内嵌卡片、输入框、代码块,比卡片略亮增加深度

文字明度四级:

  1. 亮白紫 #EDE9F7——标题、作品名、核心数据
  2. 浅紫 #C9C1E4——作品内容、副标题、说明
  3. 中紫 #9A8FC9——辅助说明、时间戳、占位符
  4. 暗紫 #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行完整歌词,更深紫背景)+点赞/分享/举报三个操作按钮。弹窗带有半透明遮罩,点击遮罩关闭。

词格LYRICRAFT广场Tab

5.2 押韵 Tab:韵脚查询 + 十大韵脚列表 + 韵脚详情

押韵 Tab 是应用的核心工具功能,为创作者提供实时韵脚查询服务。页面从上到下依次为:韵脚查询入口卡、十大高频韵脚列表。

韵脚查询入口卡:青紫背景圆角卡片,包含"输入尾字,秒查同韵词"标题(深色粗体)+"如输入「光」→ ang 韵 286 词"说明(深色半透明),模拟韵脚查询工具的入口。

十大高频韵脚列表:10 个韵脚组,每个为一行深紫卡片,左侧为韵名首字(荧光黄圆形背景)+韵名(亮白紫粗体)+示例词(中紫)+词数(霓虹粉小字),中间为韵脚词数进度条(青紫,宽度按count/3.2计算),右侧为收藏星标(已收藏★荧光黄/未收藏☆暗紫)。点击韵脚卡片弹出韵脚详情弹窗,点击星标切换收藏状态。

韵脚详情弹窗:底部弹出式半屏弹窗,包含"韵脚详情"标题(霓虹粉,字母间距3)+韵名(青紫粗体大字)+常用示例+分割线+4条押韵例句(更深紫背景,居中)+收藏韵脚/用此韵开写两个操作按钮。收藏后关闭弹窗并弹出收藏Toast。

词格LYRICRAFT押韵Tab

5.3 挑战 Tab:本月词王赛 + 挑战列表 + 挑战详情

挑战 Tab 是应用的创作激励功能,通过命题挑战激发创作者的创作欲。页面从上到下依次为:本月词王赛+我的战绩双卡片、进行中的命题列表。

本月词王赛+我的战绩双卡片:左侧荧光黄背景卡片(本月词王赛,投稿即得限定徽章),右侧深紫卡片(我的战绩,投稿6·获奖1),两个卡片横向排列。

进行中的命题列表:8 个挑战命题,每个为一行深紫卡片,左侧为主题首字(霓虹粉圆形背景)+主题名(亮白紫粗体)+状态标签(已投稿青紫/可投稿霓虹粉)+命题要求(中紫,单行省略)+剩余天数·投稿数(暗紫),下方为投稿数进度条(青紫,宽度按entries/9计算,300ms缓出动画)。点击挑战卡片弹出挑战详情弹窗。

挑战详情弹窗:底部弹出式半屏弹窗(constraintSize限高80%),包含"挑战详情"标题(霓虹粉,字母间距3)+挑战主题(亮白紫粗体)+命题(青紫)+分割线+三行信息(已有投稿/截止/评审方式,两端对齐)+热评投稿标题+3条热评投稿(更深紫背景)+投稿参赛按钮(霓虹粉,已投稿变为深灰)。点击投稿参赛关闭详情并弹出投稿弹窗。

词格LYRICRAFT挑战Tab

5.4 便签 Tab:灵感墙双列便签 + 新建/编辑/删除

便签 Tab 是应用的灵感捕捉功能,帮助创作者随时记录碎片化的灵感。页面从上到下依次为:灵感墙标题栏+新便签按钮、双列便签瀑布流。

灵感墙标题栏:左侧为"📌 灵感墙"标题(亮白紫粗体)+"置顶X条·共X条"说明(暗紫),右侧为荧光黄"+ 新便签"按钮,点击弹出新建便签弹窗。

双列便签瀑布流:使用 Flex({ wrap: FlexWrap.Wrap }) 实现双列自适应布局,8 张便签,每张为半透明背景卡片(通过 noteBg 函数着色)+半透明边框(noteBorder),包含:置顶标识📌+编辑✎+删除🗑操作栏、便签内容(亮白紫,行高18)、日期(暗紫)。每张便签通过 rotate 添加轻微旋转角度(-1.5°/1°/0.8°),模拟真实便利贴贴在墙上的手工感。点击编辑弹出编辑便签弹窗,点击删除弹出删除便签弹窗。

词格LYRICRAFT便签Tab

5.5 作品 Tab:状态筛选 + 我的词稿列表 + 编辑/删除/发布

作品 Tab 是应用的词稿管理功能,管理从草稿到发布的全流程。页面从上到下依次为:状态筛选Tab栏、词稿统计栏、我的词稿列表。

状态筛选Tab栏:三个状态标签(全部/草稿/已发布),使用 Row + 下划线 Divider 实现Tab切换,选中态为霓虹粉文字+霓虹粉下划线(宽度50%),未选中为暗紫文字+透明下划线。切换通过 statusIdx 状态管理,筛选逻辑通过 worksNow() 方法实现(同时过滤作者为"速写本"和状态)。

词稿统计栏:左侧为"共X份词稿"(暗紫),右侧为"✔ 已开启云端同步"(青紫)。

我的词稿列表:每份词稿为一行深紫卡片,左侧为状态图标(草稿✎荧光黄/已发布✓青紫,半透明荧光黄圆形背景)+词稿名《标题》(亮白紫粗体)+风格·行数·点赞数(中紫)+发布/编辑时间(暗紫),右侧为编辑✎(青紫)+操作按钮(草稿🚀发布/已发布🗑删除)。点击编辑弹出编辑词稿弹窗,点击发布弹出发布词稿弹窗,点击删除弹出删除词稿弹窗。

词格LYRICRAFT作品Tab

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条新邀约"霓虹粉+箭头,点击弹出合作邀约弹窗)、关于词格(中紫文字+箭头,点击弹出关于弹窗)、退出登录(霓虹粉文字按钮,深紫背景+边框,点击弹出退出登录弹窗)。

词格LYRICRAFT我的Tab


第六章 弹窗交互体系深度解析

6.1 弹窗设计总览

词格 LYRICRAFT 共设计十余套弹窗组件,分布在 6 个 Tab 中,涵盖作品阅读、工具查询、创作投稿、便签管理、词稿管理、个人中心六大类。所有弹窗统一采用"Stack 叠加 + if 条件渲染 + 半透明遮罩"的实现模式,底部弹出式弹窗使用半透明遮罩+底部内容,居中弹窗使用半透明遮罩+居中缩放内容。

6.2 作品抽读弹窗:底部弹出式作品阅读

作品抽读弹窗是广场 Tab 的核心交互组件,采用底部弹出式半屏设计。点击作品卡片后弹出,包含:

  • 标题区:“作 品 抽 读”(霓虹粉,字母间距3,营造设计感)
  • 作品信息:作品名(亮白紫粗体18px)+作者·风格·行数(中紫11px)
  • 作品片段:4行完整歌词引用块(更深紫背景,浅紫文字,左对齐),展示作品的核心内容
  • 操作按钮:点赞♥(霓虹粉,已点赞1.1倍缩放动画)/分享⤴(青紫)/举报⚑(中紫),三个按钮横向排列

弹窗使用半透明遮罩(rgba(10,7,22,0.7)),点击遮罩区域关闭弹窗。点赞按钮点击切换点赞状态,分享按钮点击关闭弹窗并弹出分享Toast,举报按钮点击弹出举报弹窗。

词格LYRICRAFT作品抽读弹窗

6.3 挑战详情弹窗:底部弹出式挑战信息

挑战详情弹窗是挑战 Tab 的核心交互组件,采用底部弹出式半屏设计(constraintSize限高80%防止内容过长)。点击挑战卡片后弹出,包含:

  • 标题区:“挑 战 详 情”(霓虹粉,字母间距3)
  • 挑战信息:挑战主题(亮白紫粗体17px)+命题(青紫11px)
  • 分割线
  • 信息列表:三行两端对齐信息(已有投稿X份/截止剩X天/评审方式:点赞数前3进编辑终审)
  • 热评投稿:标题+3条热评投稿(更深紫背景,浅紫文字,展示优秀投稿示例)
  • 投稿按钮:“✎ 投稿参赛”(霓虹粉,已投稿变为深灰"✓ 已投稿·查看我的")

弹窗使用半透明遮罩,点击遮罩关闭。点击投稿参赛按钮关闭详情弹窗并弹出投稿弹窗。

词格LYRICRAFT挑战详情弹窗

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 动画设计原则

深夜创作应用的动画设计遵循三个原则:

  1. 柔和缓慢:所有动画时长在180-400ms之间,缓出曲线(Curve.EaseOut),不使用快速弹跳或剧烈动画
  2. 缩放为主:主要使用缩放和透明度变化,避免位移动画造成视觉干扰
  3. 状态反馈:每个可交互元素(爱心/星标/按钮/标签)都有选中态动画,让创作者明确感知操作结果

第八章 作品广场与风格分类

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

DevEco Studio 安装界面

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

13.3 配置SDK

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 叠加实现半透明遮罩弹窗,属性动画实现交互动效。

技术亮点

  1. 单文件零依赖架构:所有代码在一个 2136.ets 文件中,无需配置路由、状态管理库、UI 组件库,开箱即用,降低理解成本和部署门槛
  2. 6 Tab + 十余弹窗完整交互闭环:从作品发现到韵脚查询、从挑战投稿到灵感记录、从词稿管理到合作邀约,形成完整的创作者旅程闭环,每个业务场景都有对应的弹窗交互
  3. 涂鸦深夜霓虹风视觉体系:三色强调色(霓虹粉/青紫/荧光黄)+ 三级背景深度 + 四级文字明度,构建出统一、有氛围感的深夜创作视觉语言
  4. 半透明便签+旋转效果:灵感便签使用半透明背景(rgba)+半透明边框+轻微旋转(-1.5°/1°/0.8°),模拟真实便利贴贴在墙上的手工感,是整个应用最具特色的视觉细节
  5. 押韵词典工具化设计:十大韵脚组+词数进度条+韵脚详情弹窗+押韵例句展示,将韵脚查询工具化,为创作者提供实时创作辅助
  6. 填词挑战命题创作:8个多样化命题挑战+投稿机制+热评展示+评审规则,通过命题约束激发创作者的创作欲,形成社区互动
  7. 纯函数便签颜色系统:三个全局纯函数(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%')
  }
}
Logo

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

更多推荐