HarmonyOS 6效果实现:ArkUI‑HorizontalAlign 水平对齐,Column 内部子元素居左、居中排布控制多卡片对齐一致性
24 台设备 · 16 条评测 · 12 条圈子帖子 · 6 Tab + 14 弹框 · GitHub 深灰终端风
HarmonyOS ArkTS 声明式 UI 单文件实现,耳机/音频设备评测 + 参数对比 + 玩家圈子
引言:当发烧友遇上终端美学
在耳机圈,"一耳朵区别"是玄学,"频响曲线"是科学,而"听感"永远是主观的。AUDIO LAB 听感实验室试图在这三者之间找到平衡——用数据说话,让耳朵做主。
这个项目不是又一个电商导购 App,而是一个面向硬核发烧友的极客终端风评测社区。整体视觉借鉴 GitHub 深色模式:#0D1117 深灰底、#58A6FF 电光蓝、#3FB950 终端绿、#D29922 琥珀警告色,搭配等宽字体的命令行式标题,让每一次滑动都像在终端里 cat 一份设备评测报告。
本文将从设计理念、色彩系统、架构设计、数据模型、六大 Tab 页面、十四个弹框、属性动画、业务模块、性能优化等维度,逐层拆解这个 2837 行的 ArkTS 单文件项目。
第一章 项目概述与设计理念
1.1 项目定位
AUDIO LAB 听感实验室是一款面向耳机发烧友和音频设备玩家的垂直社区应用,核心功能围绕三个关键词展开:
- 评测:用户发布真实听感评测,包含星级评分、标题、详细文本、点赞评论
- 对比:两台设备横向参数对比,自动生成实验室结论
- 圈子:话题化社区讨论,求推荐、设备维修、理性讨论、玄学区等
与大众消费类耳机 App 不同,AUDIO LAB 的目标用户是愿意为音质付费、对参数敏感、喜欢折腾前端和线材的硬核玩家。因此界面设计刻意避开了花哨的渐变和动效,转而采用克制、精准、信息密度高的终端风格。
1.2 设计理念:终端即实验室
整个应用的视觉语言可以概括为"终端即实验室":
- 命令行式标题:页面标题使用大号粗体白字,副标题用灰小字,模拟终端输出的层次感
- 数据优先:评分、价格、参数以等宽数字呈现,颜色编码(绿=优秀、蓝=良好、黄=及格、灰=一般)
- 卡片即窗口:每个内容卡片都有 1px 边框和圆角,像终端里的独立窗口
- 操作即命令:按钮使用纯色填充(绿=确认、蓝=主操作、红=危险),文字简洁如命令
- 状态即标签:已解决、已认证、已购等状态用小标签呈现,像 Git 仓库的 issue 标签
1.3 核心功能清单
| 模块 | 功能 | 数据量 |
|---|---|---|
| 首页 | 评测信息流、今日精选、写评测入口 | 16 条评测 |
| 榜单 | 口碑榜、分类筛选、TOP3、完整排名 | 24 台设备 |
| 对比 | 双设备参数对比、维度评分、自动结论 | 7 项对比维度 |
| 图鉴 | 设备库、品牌筛选、装备库管理 | 24 台 / 10 品牌 |
| 圈子 | 话题广场、发帖提问、帖子详情回复 | 12 帖 / 7 话题 |
| 我的 | 个人资料、获赞趋势、装备库、设置 | 4 台已购 / 5 勋章 |
1.4 技术栈与约束
- 框架:HarmonyOS ArkTS 声明式 UI
- 架构:单文件实现,@Entry 主入口 + @Component 子组件
- 状态管理:@State 组件内状态,布尔变量控制弹框显隐
- 动画:属性动画(.animation),星级点击缩放、Tab 切换缩放
- 布局:Column/Row/Stack/Flex/Scroll 组合,FlexWrap 实现网格
- 约束:无 Blank、无 UI 内变量声明、constraintSize 限高、单子组件 Scroll、接口约束全部对象字面量
第二章 色彩系统与视觉风格
2.1 配色方案:GitHub 深色模式移植
AUDIO LAB 的配色直接借鉴了 GitHub 深色模式(Dark Dimmed)的色板,并针对移动端做了微调:
| 角色 | 色值 | 用途 |
|---|---|---|
| 背景主色 | #0D1117 |
页面背景、弹框遮罩 |
| 卡片背景 | #161B22 |
内容卡片、底部 Tab 栏 |
| 深黑背景 | #0D1117 |
输入框、嵌套卡片 |
| 边框线 | #21262D / #30363D |
卡片边框、分割线 |
| 电光蓝 | #58A6FF |
主操作、链接、选中态、Tab 激活 |
| 终端绿 | #3FB950 |
确认按钮、已解决、已认证、优秀评分 |
| 琥珀黄 | #D29922 |
警告、星级、热门、及格评分 |
| 危险红 | #F85149 |
删除、错误、VS 标识 |
| 主文本 | #F0F6FC |
标题、正文 |
| 次文本 | #C9D1D9 |
正文内容、参数值 |
| 辅助文本 | #8B949E |
标签、说明、时间 |
| 占位文本 | #6E7681 / #484F58 |
placeholder、禁用态 |
2.2 评分颜色编码系统
项目中最核心的视觉语言是评分颜色编码,由 getScoreColor() 纯函数实现:
function getScoreColor(score: number): string {
if (score >= 9.2) return '#3FB950' // 旗舰级:终端绿
if (score >= 8.5) return '#58A6FF' // 优秀级:电光蓝
if (score >= 8.0) return '#D29922' // 良好级:琥珀黄
return '#8B949E' // 一般级:辅助灰
}
这套编码贯穿整个应用:榜单排名、设备卡片、对比明细、图鉴网格,所有评分数字的颜色都由这个函数统一决定。用户一眼就能通过颜色判断设备档次——绿色是旗舰(HD 800 S 9.6分、Empyrean II 9.5分),蓝色是优秀(IE 900 9.0分、DT 900 Pro X 8.9分),黄色是良好(Aria 2 8.2分、FreeBuds Pro 4 8.4分)。
2.3 终端风格细节
- 等宽字体:等级说明弹框中的
$ whoami --level命令使用fontFamily('monospace'),模拟终端命令行 - 命令式按钮:写评测、发布、确认加入等按钮使用纯色填充 + 圆角胶囊,像终端里的可执行命令
- 标签系统:
#求推荐#、#玄学区#等话题标签使用#包裹,像社交媒体的 hashtag,也像代码注释 - 状态徽章:
✓ 认证、已解决 ✓、已购等小徽章使用深色背景 + 彩色文字,像 Git 的 status check - 数据可视化:评分条使用彩色填充 + 深色背景,像终端里的进度条;获赞趋势柱状图使用蓝绿双色,高于 40 的柱子变绿
2.4 平板横屏适配
从截图可以看到,应用在平板横屏(2560×1600)下也能良好展示。由于使用了 layoutWeight(1) 和百分比宽度,页面内容会自动拉伸填充,卡片间距和字体大小保持不变,信息密度在大屏上更加舒适。底部 Tab 栏均匀分布,顶部搜索框自适应宽度。


第三章 项目架构与文件组织
3.1 单文件架构
整个项目采用单文件架构,所有代码集中在 Index.ets 中(用户标注为 2127.ets),共 2837 行。这种架构适合中小型项目,优点是:
- 无需处理模块间导入导出,开发效率高
- 全局常量和工具函数可直接访问
- 状态传递简单,通过 @State 和回调函数即可
- 便于整体阅读和调试
3.2 代码组织层次
文件内部按功能划分为清晰的层次结构:
├── 类型定义(5 个 interface)
│ ├── DeviceItem 设备数据模型
│ ├── ReviewItem 评测数据模型
│ ├── PostItem 帖子数据模型
│ ├── ChartEntry 榜单条目(接口定义,未直接使用)
│ └── TopicMeta 话题元数据
│
├── Mock 数据(4 组常量)
│ ├── mockDevices 24 台设备
│ ├── mockReviews 16 条评测
│ ├── mockPosts 12 条圈子帖子
│ └── TOPIC_CONFIG 7 个热门话题配置
│
├── 工具函数(4 个纯函数)
│ ├── getCategoryIcon 分类→emoji 图标映射
│ ├── getScoreColor 评分→颜色编码
│ ├── formatPrice 价格格式化(千分位)
│ └── getDeviceById 按 ID 查找设备
│
├── 枚举定义
│ └── LabTab 6 个 Tab 枚举
│
├── 主入口组件
│ └── AudioLabApp @Entry,Tab 切换 + 底部导航
│
├── 公共组件
│ └── TerminalHeader 终端式公共头部
│
└── Tab 页面组件(6 个)
├── HomeContent 首页·评测流(4 弹框)
├── ChartContent 榜单·口碑榜(2 弹框)
├── CompareContent 对比·参数对比(1 弹框)
├── AtlasContent 图鉴·设备库(2 弹框)
├── ForumContent 圈子·烧友社区(2 弹框)
└── MeContent 我的·个人中心(3 弹框)
3.3 组件间通信模式
项目采用单向数据流 + 回调函数的通信模式:
- 父→子:通过组件属性传递数据,如
TerminalHeader({ title: '听感实验室', subtitle: '...' }) - 子→父:通过回调函数传递事件,如
onGearTap: () => void、onSearchTap: () => void - 兄弟组件:通过父组件的 @State 中转,如对比页中
leftId和rightId的更新 - 弹框数据:通过 @State 变量传递,如
selectedDevice: DeviceItem | null、pickedReview: ReviewItem | null
3.4 弹框管理模式
14 个弹框全部采用集中式布尔变量管理:
- 每个 Tab 组件内部维护自己的弹框显隐状态(如
showWriteModal、showReviewDetail) - 弹框通过
if (this.showXxxModal) { this.xxxModal() }条件渲染 - 弹框内部使用
Stack叠加:全屏遮罩层(modalOverlay)+ 内容卡片 - 遮罩层点击关闭弹框,内容卡片点击不冒泡
- 弹框间跳转通过先关闭当前弹框再打开新弹框实现(如评测详情→分享→删除)
第四章 数据模型与全局状态管理
4.1 核心数据模型
DeviceItem(设备)
设备是整个应用的核心实体,包含 13 个字段:
interface DeviceItem {
id: number; name: string; brand: string; category: string
price: number; score: number; sound: number; comfort: number; build: number
battery: string; driver: string; tags: string[]; owned: boolean
}
- 分类体系:入耳动圈、入耳平板、入耳定制、入耳混合、头戴动圈、头戴平板、头戴开放、封闭头戴、真无线、降噪头戴、便携解码、桌面耳放、胆机、串流轿、无线头戴——覆盖从耳塞到台机的全品类
- 四维评分:sound(声音)、comfort(舒适)、build(做工)、score(综合),前三项用于对比页条形图,综合分用于榜单排名和颜色编码
- 标签系统:每台设备 2-3 个标签,如
.reference、声场王、哈曼曲线、R2R,用于图鉴页和设备详情展示 - owned 标记:布尔值标记是否在用户装备库中,4 台已购设备(U12t、IE 900、Aria 2、WH-1000XM5)
ReviewItem(评测)
interface ReviewItem {
id: number; user: string; avatar: string; device: string
rating: number; title: string; text: string
likes: number; comments: number; verified: boolean; time: string
}
16 条评测覆盖不同设备和用户视角,包含认证作者(verified=true)和普通用户。评测文本长度在 50-100 字之间,首页展示前 3 行(maxLines(3) + 省略号),点击进入详情查看全文。
PostItem(圈子帖子)
interface PostItem {
id: number; user: string; avatar: string; topic: string
text: string; likes: number; replies: number; solved: boolean
}
12 条帖子分布在 7 个话题下,solved 字段标记问题是否已解决,已解决帖子显示绿色徽章。
TopicMeta(话题元数据)
interface TopicMeta {
name: string; hot: number; posts: number
}
7 个热门话题:求推荐(hot 96, 12840 帖)、设备维修(72, 4320)、理性讨论(88, 8920)、通勤装备(81, 6540)、古典专场(64, 3210)、玄学区(92, 10240)、二手交易(58, 2890)。
4.2 数据实现方式:class implements interface
项目中所有 Mock 数据都使用 class implements interface 的方式实现,而不是直接使用对象字面量:
class DeviceItemData implements DeviceItem {
id: number = 0; name: string = ''; // ... 所有字段默认值
constructor(id, name, brand, ...) { this.id = id; this.name = name; ... }
}
const mockDevices: DeviceItem[] = [
new DeviceItemData(1, 'HD 800 S', '森海塞尔', ...),
new DeviceItemData(2, 'Empyrean II', 'Meze', ...),
// ...
]
这种方式的好处是:
- 类型安全:编译器检查构造函数参数
- 可复用:可以创建新实例而不需要重复写对象字面量
- 符合"接口约束全部对象字面量"的项目规范(接口定义,class 实现)
4.3 全局状态管理
项目没有使用全局状态管理库(如 AppStorage、LocalStorage),所有状态都是组件级 @State:
- AudioLabApp:
activeTab: LabTab—— 当前激活的 Tab - HomeContent:4 个弹框显隐 +
pickedReview+ratingPicked+reviewTitle+reviewText - ChartContent:
pickedCat+ 2 个弹框显隐 +selectedDevice - CompareContent:
leftId+rightId+showPickModal+pickingSide - AtlasContent:
pickedBrand+ 2 个弹框显隐 +selectedDevice - ForumContent:
pickedTopic+ 3 个弹框显隐 +askTitle+askBody+replyText+pickedPost - MeContent:3 个弹框显隐
这种组件级状态管理的优点是简单直接,缺点是跨 Tab 数据共享需要通过全局常量(如 mockDevices)或重新查询实现。
4.4 工具函数层
4 个纯函数构成了项目的工具层:
- getCategoryIcon(category):根据分类名称返回 emoji 图标,入耳→🎵、头戴→🎧、解码→🎚️、耳放/胆机→🎛️、串流→📡、真无线→📶、其他→🔊
- getScoreColor(score):评分颜色编码,详见第二章
- formatPrice§:价格格式化,
¥+ 千分位(toLocaleString('en-US')),如¥9,999 - getDeviceById(id):按 ID 查找设备,
return mockDevices[id - 1],用于对比页根据 leftId/rightId 获取设备详情
这些函数都是无副作用的纯函数,输入相同则输出相同,便于测试和复用。
第五章 六大 Tab 页面深度解析
5.1 Tab1:首页·评测流(HomeContent)
首页是整个应用的信息入口,采用单列信息流布局,从上到下依次为:
终端式公共头部(TerminalHeader)
- 左侧:
🎧 4 台设备+我的装备库副标题,点击进入装备库 - 中间:搜索框(
🔍 搜索设备 / 评测 / 帖子),圆角胶囊,深黑背景 - 右侧:设置按钮(
⚙️),圆形深灰背景 - 下方:大标题
听感实验室+ 副标题今日新增评测 128 条 · 认证作者 34 人
今日精选大卡
- 标题栏:
🔥 今日精选+编辑推荐 - 内容:复用
reviewCard组件,展示精选评测(mockReviews[8]——声场永动机的 Empyrean II 评测) - 精选卡有独立的橙色标题栏,与普通评测卡区分
写评测入口
- 左侧:
📝图标 +分享你的听感…占位文本 - 右侧:
写评测按钮(终端绿填充),点击打开写评测弹框 - 整行可点击,深灰卡片背景
评测信息流
ForEach(mockReviews)遍历 16 条评测,复用reviewCard组件- 每条评测卡包含:用户头像(emoji + 圆形深灰背景)、用户名、认证徽章、时间、设备名、星级评分、评测标题(电光蓝)、评测正文(3 行省略)、点赞数、评论数、
全文 ›入口 - 点击整卡打开评测详情弹框
reviewCard 组件设计
reviewCard 是首页的核心复用组件,被今日精选和信息流共同使用。它的设计要点:
- 用户信息行:头像 40×40 圆形 + 用户名粗体白字 + 认证徽章(绿底深字)+ 时间/设备灰字 + 星级(琥珀黄)
- 标题:14px 粗体电光蓝,可点击
- 正文:11px 次文本色,
maxLines(3)+TextOverflow.Ellipsis - 操作行:👍 点赞数 + 💬 评论数 +
全文 ›右对齐 - 整卡点击:打开评测详情弹框,传递
pickedReview = r
首页弹框(4 个)
- 写评测弹框(writeModal):居中弹出,包含星级选择(5 星点击,选中星放大动画)、评测标题输入、详细听感输入、同步到圈子开关、取消/发布按钮
- 评测详情弹框(reviewDetailModal):底部抽屉式,顶部圆角 24,包含完整评测内容、操作条(点赞/评论/分享/删除)、热门评论列表(4 条模拟评论)、
constraintSize({ maxHeight: '58%' })限高 - 删除确认弹框(deleteModal):居中弹出,危险红主题,
⚠️图标 + 删除确认文案 + 取消/删除按钮 - 分享弹框(shareModal):底部抽屉式,6 个分享选项(微信/复制链接/生成长图/导出MD/收藏/举报),3 列网格,取消按钮

5.2 Tab2:榜单·口碑榜(ChartContent)
榜单页是设备排名的核心展示页,采用分类筛选 + TOP3 + 完整榜单的三层结构:
分类筛选横滑
- 8 个分类:全部、入耳动圈、入耳平板、头戴平板、头戴动圈、真无线、便携解码、桌面耳放
- 横向 Scroll,选中态电光蓝填充(深字),未选中深灰卡片
pickedCat状态控制筛选,榜单列表通过if (this.pickedCat === '全部' || d.category === this.pickedCat)过滤
TOP3 领奖台
Row布局,3 列等宽,展示排名 2、1、3 的设备(注意顺序:中间是冠军🥇,左右是亚军🥈和季军🥉)- 每列:奖牌 emoji + 设备图标(64×64 圆角)+ 设备名 + 评分(颜色编码)
- 深灰卡片背景,1px 边框
- 点击打开设备详情弹框
完整榜单列表
ForEach(mockDevices)遍历 24 台设备,按分类过滤- 每行:排名数字(前 3 名琥珀黄,其他灰)+ 设备图标(40×40)+ 设备名/品牌/价格 + 评分条(彩色填充,宽度 = score×10%)+ 评分数字(颜色编码)
- 排名通过
idx + 1计算,数据本身已按评分降序排列 - 点击打开设备详情弹框
榜单弹框(2 个)
- 设备详情弹框(deviceModal):居中弹出,
constraintSize({ maxHeight: '78%' })限高,可滚动。包含设备图标(62×62)+ 设备名 + 品牌/分类/换能方式 + 价格 + 综合分大号(颜色编码)+ 三维评分条形图(声音/舒适/做工,不同颜色)+ 参数表(续航/换能方式/售价,Divider 分隔)+ 标签列表 + 近半年价格走势迷你柱状图(6 根,≤88 变绿)+ 到手价提醒/加入对比按钮 - 价格提醒弹框(priceAlertModal):居中弹出,琥珀黄主题,
🔔图标 + 说明文案 + 3 个折扣快捷按钮(9.5折/9折/8.5折)+ 设置提醒按钮(终端绿)

5.3 Tab3:对比·参数对比(CompareContent)
对比页是决策辅助工具,采用VS 大卡 + 对比明细表 + 实验室结论的结构:
VS 大卡
- 左右两列等宽,中间
VS标识(危险红,旋转 -8°) - 每列:设备图标(70×70)+ 设备名(电光蓝)+ 品牌 +
点击更换 ›入口 - 点击更换打开选择对比设备弹框,
pickingSide标记是左侧还是右侧 - 默认对比:HD 800 S(id=1)vs IE 900(id=5)
对比明细表
- 7 行对比,复用
vsRow组件:- 综合评分(高分者终端绿加粗)
- 声音(高分者绿加粗)
- 舒适(高分者绿加粗)
- 做工(高分者绿加粗)
- 售价(低价者绿加粗)
- 换能方式(无高亮,纯文本)
- 续航/供电(无高亮,纯文本)
vsRow布局:左值(右对齐)+ 标签(居中,64vp 宽)+ 右值(左对齐)- 每行之间用
Divider().color('#21262D')分隔 - 高亮逻辑通过
highlight参数控制,传入'left'/'right'/'none'
实验室结论
🤖 实验室结论标题(终端绿)- 自动生成文案:综合来看 {高分设备} 更值得入手;若预算优先,选择 {低价设备}
- 文案通过模板字符串动态生成,
getDeviceById(this.leftId).score > getDeviceById(this.rightId).score判断高分设备
对比弹框(1 个)
选择对比设备弹框(pickModal):居中弹出,标题显示选择左侧还是右侧,可滚动设备列表(constraintSize({ maxHeight: '62%' })),每行设备图标 + 名称/品牌/价格 + 选择 按钮,点击后更新 leftId 或 rightId 并关闭弹框。

5.4 Tab4:图鉴·设备库(AtlasContent)
图鉴页是设备浏览和装备库管理页,采用品牌筛选 + 设备网格 + 装备库的结构:
品牌筛选横滑
- 10 个品牌:全部、森海塞尔、HIFIMAN、七赫兹、水月雨、索尼、Apple、华为、拜雅、64 Audio
- 横向 Scroll,选中态电光蓝填充,未选中深灰卡片
pickedBrand状态控制筛选
设备网格
Flex({ wrap: FlexWrap.Wrap })实现流式网格,每个卡片宽度'45%',自动换行ForEach(mockDevices)遍历 24 台设备,按品牌过滤- 每卡:设备图标(56×56)+ 已有徽章(如果 owned)+ 设备名(1 行省略)+ 品牌/分类 + 评分(颜色编码)+ 价格
- 深灰卡片背景,1px 边框,圆角 16
- 点击打开设备详情弹框(图鉴版)
图鉴版设备详情弹框
与榜单页的设备详情弹框不同,图鉴版的设备详情更简洁:
- 设备大图(100×100)+ 设备名 + 品牌/分类 + 综合分 + 价格
- 标签列表
+ 加入装备库或已在装备库 ✓按钮(根据 owned 状态切换)- 点击加入装备库打开确认弹框
加入装备库确认弹框(ownedModal)
- 居中弹出,
➕图标 + 说明文案(加入后可记录购入价与使用时长,并同步到圈子个人页) - 暂不/确认加入按钮

5.5 Tab5:圈子·烧友社区(ForumContent)
圈子页是社区互动核心,采用话题广场 + 发帖入口 + 帖子流的结构:
热门话题横滑
🔥 热门话题标题- 横向 Scroll,7 个话题卡片,每个:
#话题名#(选中态电光蓝填充深字,未选中电光蓝字深灰底)+ 帖子数 - 点击话题切换筛选,再次点击已选中话题取消筛选(
pickedTopic = '全部') pickedTopic状态控制帖子流过滤
发帖入口
❓图标 +有问题?向 12 万烧友提问…占位文本 +提问按钮(终端绿)- 点击打开发帖提问弹框
帖子流
ForEach(mockPosts)遍历 12 条帖子,按话题过滤- 每帖:用户头像(36×36)+ 用户名 + 话题标签(
#话题#)+ 已解决徽章(如果 solved)+ 帖子正文(3 行省略)+ 👍 点赞数 + 💬 回复数 +查看全部 › - 点击打开帖子详情弹框
圈子弹框(2 个)
- 发帖提问弹框(askModal):居中弹出,
❓ 发起提问标题 + 标题输入 + 正文输入 + 话题选择(默认求推荐,可更换)+ 取消/发布按钮 - 帖子详情弹框(postModal):底部抽屉式,顶部圆角 24,包含话题标签 + 关闭按钮 + 用户信息 + 已解决徽章 + 帖子全文 + 全部回复(5 条模拟回复)+ 底部输入条(回复输入框 + 发送按钮),
constraintSize({ maxHeight: '55%' })限高

5.6 Tab6:我的·个人中心(MeContent)
我的页是用户数据和设置中心,采用资料卡 + 数据统计 + 获赞趋势 + 装备库 + 设置列表的结构:
资料卡
- 左侧:用户头像(64×64 圆形,
🎚️emoji) - 中间:用户名
频率响应 @freq_response+ 等级Lv.4 实验室常驻 · 认证作者(终端绿)+设备库 4 台 · 徽章 5 枚 - 右侧:
ℹ️信息按钮,点击打开等级说明弹框 - 深灰卡片背景,圆角 20
数据统计
- 4 列等宽:评测 34(电光蓝)、获赞 2861(终端绿)、粉丝 342(琥珀黄)、收藏 12(危险红)
- 每列:大号数字 + 小字标签
近 30 天获赞趋势
📈 近 30 天获赞趋势标题- 10 根柱状图,高度 = v×1.6 vp,≥40 的柱子变绿,其他蓝色
- 柱子底部对齐,
alignItems(VerticalAlign.Bottom)
我的装备库
🎧 我的装备库标题ForEach(mockDevices)遍历,过滤d.owned === true的设备(4 台)- 每行:设备图标(42×42)+ 设备名 + 品牌/价格 + 已购徽章(终端绿)
- 深灰卡片背景
设置列表
- 3 项设置,每项:图标 + 名称 + 右侧状态/版本 +
›箭头- 我的徽章(5/12)→ 打开勋章弹框
- 价格提醒设置(3 个生效中,琥珀黄)→ 无弹框(占位)
- 关于实验室(v4.2.1)→ 打开关于弹框
- 每项之间用
Divider().color('#21262D')分隔
我的弹框(3 个)
- 等级说明弹框(levelModal):居中弹出,终端风格,
$ whoami --level命令(等宽字体,终端绿)+ 当前等级 + 6 级等级列表(Lv.1 耳塞新手 → Lv.6 首席研究员),每行:等级编号(电光蓝,等宽)+ 等级名 + 升级条件 + 关闭按钮 - 勋章弹框(badgeModal):居中弹出,
🏅 我的勋章标题 + 5/12 已解锁 + 12 个勋章网格(4 列),已解锁勋章正常显示 + 缩放动画,未解锁勋章 20% 透明度 + 缩小,底部关闭按钮 - 关于弹框(aboutModal):居中弹出,
🎚️图标 + 应用名 + 版本号 + 简介(用数据说话,让耳朵做主 / 已收录 12,800 台设备 · 42,000 条评测)+ 用户协议/隐私政策/商务合作链接 + 关闭按钮

第六章 十四个弹框组件设计
AUDIO LAB 共包含 14 个弹框,分布在 6 个 Tab 页面中。所有弹框都采用统一的设计模式:Stack 叠加全屏遮罩 + 内容卡片,布尔变量控制显隐。
6.1 弹框设计模式
遮罩层(modalOverlay)
每个弹框都通过 @Builder modalOverlay(onClose: () => void) 生成统一的遮罩层:
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(1,4,9,0.78)')
.onClick(onClose)
}
- 遮罩颜色:
rgba(1,4,9,0.78),比页面背景更深的蓝黑色,78% 透明度 - 点击遮罩关闭弹框
- 内容卡片通过
Stack叠加在遮罩之上,点击内容卡片不触发关闭
弹框定位方式
14 个弹框分为两种定位方式:
| 定位方式 | 弹框 | 特点 |
|---|---|---|
| 居中弹出 | 写评测、删除、价格提醒、选择设备、加入装备库、等级说明、勋章、关于 | 内容较少,居中显示,宽度 78%-88% |
| 底部抽屉 | 评测详情、分享、帖子详情 | 内容较多,从底部滑出,顶部圆角 24,constraintSize 限高 |
弹框间跳转
部分弹框之间存在跳转关系,通过"先关闭当前弹框,再打开新弹框"实现:
- 评测详情 → 分享:点击分享按钮,
showReviewDetail = false; showShareModal = true - 评测详情 → 删除:点击删除按钮,
showReviewDetail = false; showDeleteModal = true - 设备详情 → 价格提醒:点击到手价提醒,
showDeviceModal = false; showPriceAlert = true - 图鉴设备详情 → 加入装备库:点击加入按钮,
showDeviceModal = false; showOwnedModal = true
6.2 弹框清单与功能
首页弹框(4 个)
-
写评测弹框(writeModal)
- 星级选择:5 颗星,点击设置
ratingPicked,选中星放大 1.25 倍动画 - 评测标题:TextInput,单行
- 详细听感:TextInput,多行(64vp 高)
- 同步到圈子:已开启开关(展示用,无实际交互)
- 操作:取消(深灰)+ 发布(终端绿)
- 星级选择:5 颗星,点击设置
-
评测详情弹框(reviewDetailModal)
- 底部抽屉,
constraintSize({ maxHeight: '58%' }) - 标题栏:评测详情 + ❌ 关闭
- 用户信息:头像 + 用户名 + 认证徽章 + 时间/设备 + 星级
- 评测标题 + 全文
- 操作条:👍 点赞 + 💬 评论 + ↗ 分享 + 🗑 删除
- 热门评论:4 条模拟评论,每条头像 + 用户名 + 评论文本 + 👍
- 底部抽屉,
-
删除确认弹框(deleteModal)
- 居中弹出,危险红主题
- ⚠️ 大号图标 +
删除这条评测?(危险红粗体) - 警告说明:删除后点赞与评论数据将一并清除,且无法恢复
- 操作:取消(深灰)+ 删除(危险红填充)
-
分享弹框(shareModal)
- 底部抽屉
- 6 个分享选项,3 列 FlexWrap 网格:微信💬、复制链接🔗、生成长图📜、导出MD📤、收藏⭐、举报🚩
- 每个选项:圆形图标(48×48)+ 标签文字
- 取消按钮
榜单弹框(2 个)
-
设备详情弹框(deviceModal)
- 居中弹出,
constraintSize({ maxHeight: '78%' }),可滚动 - 设备头图:图标(62×62)+ 设备名 + 品牌/分类/换能方式 + 价格 + ❌ 关闭
- 综合分 + 三维评分条形图:
- 声音:电光蓝条,宽度 = sound×10%
- 舒适:终端绿条,宽度 = comfort×10%
- 做工:琥珀黄条,宽度 = build×10%
- 参数表:续航/供电、换能方式、参考售价,Divider 分隔
- 标签列表:
#标签横向排列 - 近半年价格走势:6 根迷你柱状图,≤88 变绿(表示降价)
- 操作:🔔 到手价提醒(深灰)+ 加入对比(电光蓝)
- 居中弹出,
-
价格提醒弹框(priceAlertModal)
- 居中弹出,琥珀黄主题
- 🔔 图标 +
到手价提醒标题 + 说明文案 - 3 个折扣快捷按钮:9.5折、9折、8.5折(深灰背景,琥珀黄文字)
- 设置提醒按钮(终端绿)
对比弹框(1 个)
- 选择对比设备弹框(pickModal)
- 居中弹出,
constraintSize({ maxHeight: '62%' }),可滚动 - 标题:选择设备(左侧/右侧),根据
pickingSide动态显示 - 设备列表:24 台设备,每行图标 + 名称/品牌/价格 + 选择按钮
- 点击选择后更新
leftId或rightId并关闭
- 居中弹出,
图鉴弹框(2 个)
-
图鉴版设备详情弹框(deviceModal)
- 与榜单页设备详情同名但实现不同,更简洁
- 设备大图(100×100)+ 设备名 + 品牌/分类 + 综合分 + 价格
- 标签列表
- 装备库按钮:未拥有显示
+ 加入装备库(电光蓝填充),已拥有显示已在装备库 ✓(深灰背景绿字) - 点击加入装备库打开确认弹框
-
加入装备库确认弹框(ownedModal)
- 居中弹出
- ➕ 图标 +
加入我的装备库标题 + 说明文案(加入后可记录购入价与使用时长,并同步到圈子个人页) - 操作:暂不(深灰)+ 确认加入(终端绿)
圈子弹框(2 个)
-
发帖提问弹框(askModal)
- 居中弹出
- ❓
发起提问标题 - 标题输入:TextInput,单行
- 正文输入:TextInput,多行(56vp 高)
- 话题选择:
话题:求推荐+更换 ›(展示用) - 操作:取消(深灰)+ 发布(终端绿)
-
帖子详情弹框(postModal)
- 底部抽屉,
constraintSize({ maxHeight: '55%' }) - 标题栏:
#话题#(电光蓝粗体)+ ❌ 关闭 - 用户信息:头像 + 用户名 + 已解决徽章
- 帖子全文
- 全部回复:5 条模拟回复,每条头像 + 用户名 + 回复文本 + 👍
- 底部输入条:回复输入框 + 发送按钮(终端绿)
- 底部抽屉,
我的弹框(3 个)
-
等级说明弹框(levelModal)
- 居中弹出,终端风格
$ whoami --level命令(等宽字体,终端绿)- 当前等级:
Lv.4 实验室常驻(大号粗体) - 6 级等级列表,每行:等级编号(电光蓝,等宽字体,44vp 宽)+ 等级名 + 升级条件
- Lv.1 耳塞新手:注册即达
- Lv.2 数据熟手:发布 3 条评测
- Lv.3 听感老鸟:获得 50 赞
- Lv.4 实验室常驻:获得 500 赞(当前)
- Lv.5 金耳朵认证:评测被官方精选
- Lv.6 首席研究员:年度TOP作者
- 关闭按钮(深灰背景,电光蓝文字)
-
勋章弹框(badgeModal)
- 居中弹出
- 🏅
我的勋章标题 +5 / 12 已解锁 - 12 个勋章网格,4 列 FlexWrap:
- 已解锁(5 个):正常显示 + 1.06 倍缩放动画
- 未解锁(7 个):20% 透明度 + 0.94 倍缩小
- 关闭按钮
-
关于弹框(aboutModal)
- 居中弹出
- 🎚️ 大号图标 + 应用名
听感实验室 AUDIO LAB+ 版本号Version 4.2.1 (Build 2048) - 简介:
用数据说话,让耳朵做主+已收录 12,800 台设备 · 42,000 条评测 - 链接:用户协议 · 隐私政策 · 商务合作(电光蓝)
- 关闭按钮
第七章 属性动画与交互体验
AUDIO LAB 的动画设计遵循克制、精准、功能性的原则,没有花哨的转场特效,所有动画都服务于交互反馈。
7.1 动画清单
项目中共使用 3 处属性动画,全部通过 .animation() 修饰符实现:
| 动画位置 | 触发条件 | 动画效果 | 参数 |
|---|---|---|---|
| 底部 Tab 图标 | 切换 Tab | 图标放大 1.16 倍 | duration 180ms, EaseOut |
| 写评测星级 | 点击星级 | 选中星放大 1.25 倍 | duration 160ms, EaseOut |
| 勋章弹框 | 展示勋章 | 已解锁勋章 1.06 倍缩放 | duration 200ms, EaseOut |
7.2 底部 Tab 切换动画
Text(icon)
.fontSize(19)
.opacity(this.activeTab === tab ? 1.0 : 0.4)
.scale({ x: this.activeTab === tab ? 1.16 : 1.0, y: this.activeTab === tab ? 1.16 : 1.0 })
.animation({ duration: 180, curve: Curve.EaseOut })
- 激活 Tab:图标不透明度 1.0 + 放大 1.16 倍 + 标签电光蓝粗体 + 底部 18×3 电光蓝指示条
- 未激活 Tab:图标不透明度 0.4 + 原始大小 + 标签灰字
- 动画时长 180ms,EaseOut 曲线,快速弹出后自然减速
- 6 个 Tab 均匀分布,
layoutWeight(1)
7.3 星级点击动画
写评测弹框中的星级选择使用了点击放大动画:
Text(s <= this.ratingPicked ? '★' : '☆')
.fontSize(24)
.fontColor(s <= this.ratingPicked ? '#D29922' : '#30363D')
.scale({ x: s === this.ratingPicked ? 1.25 : 1.0, y: s === this.ratingPicked ? 1.25 : 1.0 })
.animation({ duration: 160, curve: Curve.EaseOut })
.onClick(() => { this.ratingPicked = s })
- 选中星:琥珀黄实心 ★ + 放大 1.25 倍
- 未选中星:深灰空心 ☆
- 当前选中的星(
s === this.ratingPicked)额外放大,提供明确的点击反馈 - 动画时长 160ms,比 Tab 切换更快,符合星级选择的高频交互特性
7.4 勋章缩放动画
勋章弹框中,已解锁和未解锁勋章使用不同的缩放比例:
Text(b[0])
.opacity(b[1] === '1' ? 1.0 : 0.2)
.scale({ x: b[1] === '1' ? 1.06 : 0.94, y: b[1] === '1' ? 1.06 : 0.94 })
.animation({ duration: 200, curve: Curve.EaseOut })
- 已解锁:不透明度 1.0 + 放大 1.06 倍(轻微突出)
- 未解锁:不透明度 0.2 + 缩小 0.94 倍(视觉退后)
- 动画时长 200ms,是项目中最慢的动画,因为勋章展示不需要快速响应
7.5 无动画的交互
项目中大部分交互没有使用动画,这是刻意的设计选择:
- 弹框显隐:直接条件渲染,无淡入淡出/缩放动画
- 分类/品牌/话题筛选:直接切换,无过渡动画
- 列表滚动:使用系统默认 Scroll 行为
- 按钮点击:无按压效果(无 scale 动画)
这种"少即是多"的动画策略符合终端风格的克制美学,也提升了应用的响应速度和性能。
7.6 交互反馈的其他方式
除了属性动画,项目还通过以下方式提供交互反馈:
- 颜色变化:选中态使用填充背景(电光蓝/终端绿),未选中使用深灰卡片
- 不透明度变化:Tab 图标激活 1.0 / 未激活 0.4,勋章已解锁 1.0 / 未解锁 0.2
- 边框变化:选中态可能有边框高亮(本项目主要用背景色区分)
- 文字样式:激活 Tab 标签粗体 + 电光蓝,未激活常规 + 灰
- 指示条:底部 Tab 激活时有 18×3 电光蓝指示条
第八章 设备评测系统与口碑榜
8.1 评测系统设计
评测是 AUDIO LAB 的核心内容,16 条评测覆盖了不同设备类型和用户视角:
评测数据结构
每条评测包含:
- 用户信息:用户名、头像(emoji)、认证状态
- 设备关联:评测的设备名称
- 评分:1-5 星(rating)
- 内容:标题(title)+ 正文(text)
- 互动:点赞数(likes)、评论数(comments)
- 时间:相对时间(2小时前、1天前等)
评测内容特点
16 条评测的内容设计体现了发烧友社区的真实氛围:
- 技术流:“换上平衡线之后的新世界”——讨论平衡线对 HD800S 的影响
- 体验流:“通勤党一年后的真实反馈”——WH-1000XM5 的长期使用体验
- 性价比流:“百元价位还有对手吗”——Aria 2 的入门推荐
- 玄学流:“300B胆机的浪漫”——WA22 胆机的主观听感
- 数据流:“接近哈曼曲线的国产良心”——Blessing 3 的频响分析
- 生态流:“苹果生态的粘性有多强”——AirPods Pro 2 的生态体验
评测展示策略
- 首页信息流:展示全部 16 条,按时间倒序(最新在前)
- 今日精选:置顶 1 条精选评测(mockReviews[8]——Empyrean II 评测),有独立的橙色标题栏
- 卡片展示:头像 + 用户名 + 认证 + 时间/设备 + 星级 + 标题(电光蓝)+ 正文(3 行省略)+ 点赞/评论/全文入口
- 详情展示:点击进入底部抽屉弹框,展示全文 + 操作条 + 热门评论
8.2 口碑榜算法
口碑榜基于 24 台设备的综合评分(score)排名,数据本身已按评分降序排列:
| 排名 | 设备 | 品牌 | 评分 | 价格 | 分类 |
|---|---|---|---|---|---|
| 1 | HD 800 S | 森海塞尔 | 9.6 | ¥9,999 | 头戴动圈 |
| 2 | Empyrean II | Meze | 9.5 | ¥24,999 | 头戴平板 |
| 3 | U12t | 64 Audio | 9.4 | ¥16,999 | 入耳定制 |
| 4 | MEST MkII | qdc | 9.2 | ¥12,800 | 入耳混合 |
| 5 | HE1000 Stealth | HIFIMAN | 9.3 | ¥8,999 | 头戴平板 |
| … | … | … | … | … | … |
注意:数据中 HE1000 Stealth 的 score 是 9.3,但排在第 5 位(应该在 U12t 9.4 之后、MEST MkII 9.2 之前,即第 4 位)。这是 Mock 数据的小瑕疵,实际应用中应通过排序函数动态排序。
榜单展示层次
- TOP3 领奖台:3 列等宽,中间冠军🥇,左右亚军🥈和季军🥉,展示奖牌 + 设备图标 + 名称 + 评分
- 完整榜单:24 台设备逐条列出,每行排名 + 图标 + 名称/品牌/价格 + 评分条 + 评分数字
- 分类筛选:8 个分类横滑,筛选后只显示对应分类的设备
评分条可视化
榜单每行的评分条使用彩色填充:
Column() {
Column()
.width((d.score * 10) + '%') // 9.6分 → 96%宽度
.height(5)
.backgroundColor(getScoreColor(d.score)) // 颜色编码
.borderRadius(3)
}
.width('100%').height(5)
.backgroundColor('#21262D').borderRadius(3)
- 背景:深灰
#21262D - 填充:评分×10% 宽度,颜色由
getScoreColor()决定 - 评分数字在右侧,同样颜色编码
8.3 设备详情页
设备详情弹框是设备信息的集中展示页,包含:
头部信息
- 设备图标(62×62)
- 设备名(17px 粗体)
- 品牌 · 分类 · 换能方式(灰小字)
- 价格(终端绿粗体)
- 关闭按钮
评分区
- 综合分大号(34px,颜色编码)
- 三维评分条形图:
- 声音(电光蓝):sound 分,条形宽度 = sound×10%
- 舒适(终端绿):comfort 分
- 做工(琥珀黄):build 分
- 注意:便携解码/耳放/胆机类设备的 sound/comfort/build 为 0,不显示评分条(实际代码中仍会显示 0 宽度的条)
参数表
- 续航/供电:有线 / 30小时 / 常供电 等
- 换能方式:38mm / 平板 / 12单元 / R2R 等
- 参考售价:终端绿粗体
标签与价格走势
- 标签列表:
#.reference#声场王等 - 近半年价格走势:6 根迷你柱状图,模拟价格变化,≤88(相对值)变绿表示降价
操作按钮
- 🔔 到手价提醒:打开价格提醒弹框
- 加入对比:电光蓝填充,关闭弹框(实际应加入对比列表,本项目为展示用)
第九章 参数对比与设备图鉴
9.1 参数对比系统
对比页是 AUDIO LAB 的决策辅助核心,支持两台设备的横向对比。
对比维度
7 项对比维度分为三类:
- 评分维度(有胜负):综合评分、声音、舒适、做工——高分者终端绿加粗
- 价格维度(有胜负):售价——低价者终端绿加粗
- 参数维度(无胜负):换能方式、续航/供电——纯文本展示,无高亮
vsRow 组件设计
vsRow 是对比明细的复用组件:
@Builder vsRow(label: string, leftVal: string, rightVal: string, highlight: string) {
Row() {
Text(leftVal) // 左值,右对齐
.fontWeight(highlight === 'left' ? FontWeight.Bold : FontWeight.Normal)
.fontColor(highlight === 'left' ? '#3FB950' : '#C9D1D9')
.layoutWeight(1).textAlign(TextAlign.End)
Text(label) // 中间标签,居中,64vp 宽
.fontSize(9).fontColor('#6E7681')
.width(64).textAlign(TextAlign.Center)
Text(rightVal) // 右值,左对齐
.fontWeight(highlight === 'right' ? FontWeight.Bold : FontWeight.Normal)
.fontColor(highlight === 'right' ? '#3FB950' : '#C9D1D9')
.layoutWeight(1).textAlign(TextAlign.Start)
}
.width('100%').padding({ top: 10, bottom: 10 })
}
- 布局:左值(layoutWeight 1,右对齐)+ 标签(64vp,居中)+ 右值(layoutWeight 1,左对齐)
- 高亮:通过
highlight参数控制,'left'表示左方胜出(终端绿加粗),'right'表示右方胜出,'none'表示无高亮 - 每行之间用
Divider().color('#21262D')分隔
自动结论生成
对比页底部的"实验室结论"通过模板字符串动态生成:
Text('综合来看 ' + (getDeviceById(this.leftId).score > getDeviceById(this.rightId).score ?
getDeviceById(this.leftId).name : getDeviceById(this.rightId).name) +
' 更值得入手;若预算优先,选择 ' +
(getDeviceById(this.leftId).price < getDeviceById(this.rightId).price ?
getDeviceById(this.leftId).name : getDeviceById(this.rightId).name) + '。')
- 综合推荐:评分高的设备
- 预算推荐:价格低的设备
- 结论文案自动适配当前对比的两台设备
设备选择
- 默认对比:HD 800 S(id=1)vs IE 900(id=5)
- 点击"点击更换"打开选择设备弹框,
pickingSide标记左侧/右侧 - 选择设备后更新
leftId或rightId,对比明细和结论自动刷新 - 通过
getDeviceById(id)函数根据 ID 查找设备对象
9.2 设备图鉴系统
图鉴页是设备浏览和装备库管理页,24 台设备以网格形式展示。
品牌筛选
- 10 个品牌横滑:全部、森海塞尔、HIFIMAN、七赫兹、水月雨、索尼、Apple、华为、拜雅、64 Audio
- 选中态电光蓝填充,未选中深灰卡片
pickedBrand状态控制筛选
网格布局
图鉴页使用 Flex({ wrap: FlexWrap.Wrap }) 实现流式网格:
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(mockDevices, (d: DeviceItem) => {
if (this.pickedBrand === '全部' || d.brand === this.pickedBrand) {
Column() { /* 设备卡片 */ }
.width('45%') // 每卡 45% 宽度,自动换行
.margin(6)
}
})
}
- 每卡宽度
'45%',左右 margin 6vp,实际两列布局 FlexWrap.Wrap自动换行,不需要固定列数- 设备卡片:图标(56×56)+ 已有徽章 + 设备名(1 行省略)+ 品牌/分类 + 评分(颜色编码)+ 价格
装备库管理
- 每台设备有
owned: boolean字段,标记是否在用户装备库中 - 已拥有设备在图鉴卡片右上角显示"已有"徽章(终端绿)
- 我的页展示装备库列表(4 台已购设备:U12t、IE 900、Aria 2、WH-1000XM5)
- 点击设备打开图鉴版详情弹框,可加入装备库(确认弹框)
图鉴版设备详情
与榜单页的设备详情不同,图鉴版更简洁:
- 设备大图(100×100)
- 设备名 + 品牌/分类
- 综合分 + 价格
- 标签列表
- 装备库按钮(加入/已拥有状态切换)
第十章 烧友圈子与社区互动
10.1 圈子系统设计
圈子是 AUDIO LAB 的社区互动核心,采用话题化社区模式,用户可以在不同话题下发帖讨论。
话题体系
7 个热门话题覆盖发烧友社区的典型讨论场景:
| 话题 | 热度 | 帖子数 | 典型内容 |
|---|---|---|---|
| 求推荐 | 96 | 12,840 | 预算内设备推荐、搭配求助 |
| 玄学区 | 92 | 10,240 | 线材、煲机、脑放等玄学讨论 |
| 理性讨论 | 88 | 8,920 | 频响曲线、参数、技术分析 |
| 通勤装备 | 81 | 6,540 | 降噪、真无线、便携设备 |
| 设备维修 | 72 | 4,320 | 故障排查、维修教程、配件 |
| 古典专场 | 64 | 3,210 | 古典音乐设备搭配、录音版本 |
| 二手交易 | 58 | 2,890 | 收/出设备、闲鱼担保交易 |
话题展示
- 热门话题横滑:每个话题卡片显示
#话题名#+ 帖子数 - 选中态:电光蓝填充背景 + 深字
- 未选中:深灰背景 + 电光蓝字
- 点击切换筛选,再次点击已选中话题取消筛选(回到全部)
帖子数据结构
interface PostItem {
id: number; user: string; avatar: string; topic: string
text: string; likes: number; replies: number; solved: boolean
}
topic:帖子所属话题,用于筛选solved:问题是否已解决,已解决帖子显示绿色徽章replies:回复数,帖子详情展示模拟回复列表
帖子内容特点
12 条帖子覆盖了社区的典型讨论:
- 求推荐类:“预算2000以内,主要听流行人声,在Aria 2和Timeless之间纠结”
- 维修类:“HD600头梁开裂,官方维修报价800,自己换头梁教程有吗?”
- 理性讨论类:“频响曲线真的能代表听感吗?为什么两根曲线几乎重合的塞子听感差这么多?”
- 玄学类:“盲听测试连败十次之后,我把3000块的升级线挂闲鱼了”
- 退烧类:“从3W的桌面系统退回手机+小尾巴,突然发现自己其实听不出那么大差距”
- 经验类:“给年轻人一句话:先买好塞子,再考虑前端,最后才是线材”
10.2 发帖系统
发帖入口
- 圈子页顶部:
❓图标 +有问题?向 12 万烧友提问…+提问按钮(终端绿) - 点击打开发帖提问弹框
发帖弹框
- 标题输入:TextInput,单行,placeholder “一句话说清你的问题…”
- 正文输入:TextInput,多行(56vp 高),placeholder “预算 / 设备 / 听音风格…”
- 话题选择:默认"求推荐",可更换(展示用,无实际交互)
- 操作:取消(深灰)+ 发布(终端绿)
10.3 帖子详情与回复
帖子详情弹框
底部抽屉式,constraintSize({ maxHeight: '55%' }):
- 标题栏:
#话题#(电光蓝粗体)+ ❌ 关闭 - 用户信息:头像 + 用户名 + 已解决徽章(如果 solved)
- 帖子全文
- 全部回复:5 条模拟回复,每条头像 + 用户名 + 回复文本 + 👍
- 底部输入条:回复输入框 + 发送按钮(终端绿)
回复系统
- 模拟回复数据:5 条固定回复,覆盖不同视角(老烧建议、同问、感谢、提醒、补充)
- 回复展示:头像(emoji)+ 用户名 + 回复文本 + 点赞按钮
- 输入条:TextInput + 发送按钮,点击发送关闭弹框(实际应提交回复,本项目为展示用)
10.4 社区互动数据
- 今日新帖:342 条
- 采纳率:78%
- 12 万烧友:社区总用户数(展示用)
- 已解决帖子:显示绿色"已解决 ✓"徽章,提供明确的问题解决状态
第十一章 个人中心与等级体系
11.1 个人资料卡
我的页顶部是个人资料卡,展示用户核心信息:
- 头像:64×64 圆形,
🎚️emoji(调音台图标,符合音频主题) - 用户名:
频率响应 @freq_response(16px 粗体白字) - 等级与认证:
Lv.4 实验室常驻 · 认证作者(终端绿,10px) - 设备与徽章:
设备库 4 台 · 徽章 5 枚(灰字,9px) - 信息按钮:
ℹ️,点击打开等级说明弹框
11.2 数据统计
资料卡下方是 4 列数据统计:
| 指标 | 数值 | 颜色 |
|---|---|---|
| 评测 | 34 | 电光蓝 #58A6FF |
| 获赞 | 2,861 | 终端绿 #3FB950 |
| 粉丝 | 342 | 琥珀黄 #D29922 |
| 收藏 | 12 | 危险红 #F85149 |
每列:大号数字(15px 粗体)+ 小字标签(9px 灰),layoutWeight(1) 均匀分布。
11.3 获赞趋势图
近 30 天获赞趋势使用柱状图可视化:
- 10 根柱子,模拟 30 天中的 10 个数据点
- 柱子高度 = v × 1.6 vp(v 为获赞数,范围 8-48)
- 颜色:≥40 的柱子变绿(表示高获赞),其他蓝色
- 布局:
Row+alignItems(VerticalAlign.Bottom),柱子底部对齐 - 每根柱子:
layoutWeight(1)均匀分布,宽度 10vp,顶部圆角 3
11.4 装备库
我的页展示用户已购设备列表:
- 标题:
🎧 我的装备库 - 遍历
mockDevices,过滤d.owned === true的设备 - 4 台已购设备:U12t(64 Audio,¥16,999)、IE 900(森海塞尔,¥6,999)、Aria 2(水月雨,¥799)、WH-1000XM5(索尼,¥2,499)
- 每行:设备图标(42×42)+ 设备名 + 品牌/价格 + 已购徽章(终端绿)
- 深灰卡片背景,圆角 14
11.5 等级体系
6 级等级
| 等级 | 名称 | 升级条件 |
|---|---|---|
| Lv.1 | 耳塞新手 | 注册即达 |
| Lv.2 | 数据熟手 | 发布 3 条评测 |
| Lv.3 | 听感老鸟 | 获得 50 赞 |
| Lv.4 | 实验室常驻 | 获得 500 赞(当前) |
| Lv.5 | 金耳朵认证 | 评测被官方精选 |
| Lv.6 | 首席研究员 | 年度TOP作者 |
等级说明弹框
- 终端风格设计:
$ whoami --level命令(等宽字体,终端绿) - 当前等级大号显示:
Lv.4 实验室常驻 - 6 级等级列表,每行:等级编号(电光蓝,等宽字体,44vp 宽)+ 等级名 + 升级条件
- 关闭按钮:深灰背景,电光蓝文字
11.6 勋章系统
12 枚勋章
- 已解锁 5 枚:🎧(设备评测)、📊(数据分析)、🔧(维修帮助)、📻(社区活跃)、🎓(知识分享)
- 未解锁 7 枚:🥇(年度最佳)、🧪(实验精神)、💎(钻石会员)、🔭(深度评测)、⚖️(公正评价)、🎯(精准推荐)、👑(社区之王)
勋章弹框
- 标题:
🏅 我的勋章+5 / 12 已解锁 - 4 列 FlexWrap 网格,12 枚勋章
- 已解锁:正常显示 + 1.06 倍缩放动画 + "已解锁"标签(终端绿)
- 未解锁:20% 透明度 + 0.94 倍缩小 + "未解锁"标签(深灰)
- 关闭按钮
11.7 设置列表
3 项设置,每项:图标 + 名称 + 右侧状态/版本 + › 箭头:
- 我的徽章:右侧
5/12→ 打开勋章弹框 - 价格提醒设置:右侧
3 个生效中(琥珀黄)→ 无弹框(占位) - 关于实验室:右侧
v4.2.1→ 打开关于弹框
每项之间用 Divider().color('#21262D') 分隔。
11.8 关于弹框
- 🎚️ 大号图标
- 应用名:
听感实验室 AUDIO LAB(16px 粗体) - 版本号:
Version 4.2.1 (Build 2048)(10px 灰) - 简介:
用数据说话,让耳朵做主+已收录 12,800 台设备 · 42,000 条评测(11px 次文本,居中) - 链接:
用户协议 · 隐私政策 · 商务合作(电光蓝,10px) - 关闭按钮
第十二章 性能优化与扩展方向
12.1 当前性能特点
单文件架构的优势
- 启动快:无需模块加载,所有代码在一个文件中,编译后直接运行
- 内存占用低:无额外的模块管理开销,全局常量和函数直接访问
- 渲染高效:ArkTS 声明式 UI 的 diff 算法在单文件中更容易优化
- 开发效率高:无需处理导入导出,状态传递简单
列表渲染优化
ForEach的第二个参数(key 函数)确保列表项的唯一标识,如(d: DeviceItem) => d.id.toString()- 筛选场景下 key 包含筛选条件,如
(d: DeviceItem) => d.id.toString() + this.pickedCat,确保筛选切换时正确重建列表 maxLines(3)+TextOverflow.Ellipsis限制长文本渲染,避免布局计算开销
动画性能
- 仅 3 处属性动画,且都是简单的 scale 变换,GPU 加速友好
- 无复杂的转场动画、物理动画、手势动画
- 动画时长 160-200ms,短平快,不影响交互响应
12.2 可优化点
数据层面
- 大列表虚拟化:当前 24 台设备、16 条评测、12 条帖子数据量小,无需虚拟化。若扩展到数百条,应使用
LazyForEach实现懒加载 - 图片资源:当前使用 emoji 作为设备图标和用户头像,零网络请求。若替换为真实图片,应使用
Image的alt、offset等属性优化加载体验 - 数据缓存:当前所有数据为内存常量,重启后丢失。可接入
Preferences或数据库持久化用户装备库、评测草稿等
组件层面
- 组件拆分:当前单文件 2837 行,可按功能拆分为多个文件(如
components/、models/、utils/、pages/),提升可维护性 - 自定义组件复用:
reviewCard、vsRow、modalOverlay等已通过@Builder复用,可进一步抽取为独立@Component组件 - 状态管理:当前使用组件级
@State,跨 Tab 数据共享不便。可引入AppStorage或Provide/Inject实现全局状态管理
交互层面
- 下拉刷新:评测流、帖子流可增加下拉刷新,模拟数据更新
- 无限滚动:当前数据量固定,可扩展为分页加载
- 手势操作:可增加左滑删除、长按菜单等手势交互
- 搜索功能:当前搜索框为展示用,可实现真实的设备/评测/帖子搜索
12.3 扩展方向
功能扩展
- 设备对比增强:支持 3 台以上设备对比、雷达图可视化、历史价格曲线
- 评测系统增强:评测维度评分(声音/舒适/做工/性价比)、评测图片上传、评测视频
- 社区增强:关注系统、私信、通知推送、社区等级与勋章联动
- 电商接入:设备购买链接、价格历史追踪、降价提醒推送
- 个人数据:听音时长统计、设备使用频率、听感偏好分析
技术扩展
- 多端适配:当前已适配平板横屏,可进一步适配折叠屏、车机、智慧屏
- 国际化:当前为中文界面,可扩展多语言支持
- 深色/浅色模式:当前为固定深色模式,可支持系统主题切换
- 无障碍:增加屏幕阅读器支持、颜色对比度优化、字体大小适配
- 性能监控:接入性能监控 SDK,跟踪页面加载时间、帧率、内存占用
数据扩展
- 真实数据接入:当前为 Mock 数据,可接入后端 API 获取真实设备库、评测、社区数据
- 用户生成内容:支持用户发布评测、帖子、回复,数据持久化到后端
- 设备数据库:扩展到数百台设备,支持按品牌、分类、价格、评分多维度筛选
- 评测数据聚合:基于用户评测计算设备综合评分、维度评分,生成口碑榜
第十三章 音频评测知识与选购指南
13.1 耳机分类体系
AUDIO LAB 的设备分类覆盖了从耳塞到台机的全品类,理解这些分类是选购的第一步:
按佩戴方式
- 入耳式(IEM):塞入耳道,隔音好,便携。细分:动圈、动铁、平板、混合单元、定制
- 头戴式(Headphone):覆盖耳朵,声场大,适合家用。细分:开放(漏音,声场自然)、封闭(隔音,适合通勤)、降噪
- 真无线(TWS):无线连接,便携性最强。细分:降噪、非降噪
按换能方式
- 动圈(Dynamic):最常见,振膜+音圈,低频好,氛围感强。代表:HD 800 S(38mm)、IE 900(7mm)、Aria 2(10mm)
- 动铁(Balanced Armature):体积小,解析高,低频量少。代表:ER2SE、U12t(12单元)
- 平板(Planar Magnetic):振膜轻薄,瞬态好,声场大。代表:Empyrean II、Ananda、HE1000 Stealth、Timeless(14.6mm)
- 混合(Hybrid):动圈+动铁+骨传导等组合,兼顾各频段。代表:MEST MkII
按用途
- 监听(Monitor):音染少,还原真实,适合专业工作。代表:DT 900 Pro X、U12t
- HiFi:追求音质,有调音风格。代表:HD 800 S、Empyrean II
- 降噪(ANC):主动降噪,适合通勤。代表:WH-1000XM5、AirPods Pro 2、FreeBuds Pro 4
- 游戏:低延迟,麦克风,声场定位。本项目未收录
13.2 前端设备分类
除了耳机,AUDIO LAB 还收录了前端设备(解码/耳放/胆机/串流):
- 便携解码(小尾巴):手机/电脑外接,提升音质。代表:M9701(飞傲,R2R)、RU7(山灵,R2R)、QDX(凯音,ES9038Pro)
- 桌面耳放:驱动大耳,推力大。代表:A90 Discrete(Topping,分立电路)
- 胆机:电子管放大,胆味浓,中频暖。代表:WA22(Woo Audio,三胆)
- 串流轿:网络音频播放,支持流媒体。代表:Zen Stream(iFi)
13.3 评分维度解读
AUDIO LAB 使用四维评分体系:
综合评分(score)
- 范围:0-10,本项目设备在 7.8-9.6 之间
- 颜色编码:≥9.2 绿(旗舰)、≥8.5 蓝(优秀)、≥8.0 黄(良好)、其他灰(一般)
- 综合评分是主观评价的汇总,参考价值高但不应是唯一标准
声音(sound)
- 评价音质的核心维度,包括解析力、声场、三频分布、瞬态、动态等
- 旗舰设备通常在 9.0 以上:HD 800 S(9.8)、HE1000 Stealth(9.6)、U12t(9.5)
- 前端设备(解码/耳放/胆机)的 sound 为 0,因为它们不直接发声
舒适(comfort)
- 佩戴舒适度,包括重量、夹头感、耳道适配、长时间佩戴疲劳等
- 真无线和降噪头戴通常舒适度高:AirPods Pro 2(9.0)、WH-1000XM5(8.8)、FreeBuds Pro 4(8.9)
- 深入耳设计舒适度较低:ER2SE(7.5,三节套深入耳,戴两小时耳朵会抗议)
做工(build)
- 制造质量,包括材质、工艺、耐用性、设计感等
- 旗舰和专业设备做工通常好:WH-1000XM5(9.4)、FreeBuds Pro 4(9.1)、DT 900 Pro X(9.2)、IE 900(9.0)
- 老旗舰和性价比设备做工可能一般:RS 220(7.0)、Aria 2(7.8)
13.4 选购建议
按预算
- 千元内:Aria 2(¥799,水月雨,入门首选,性价比高)、KG770(¥999,AKG,监听入门)
- 1-3 千:Timeless(¥1,599,七赫兹,千元平板)、ER2SE(¥1,299,Etymotic,隔音王)、WH-1000XM5(¥2,499,索尼,降噪标杆)、AirPods Pro 2(¥1,899,Apple,生态联动)
- 3-8 千:IE 900(¥6,999,森海塞尔,动圈旗舰)、DT 900 Pro X(¥3,699,拜雅,监听)、Ananda(¥4,299,HIFIMAN,性价比平板)、A90 Discrete(¥4,299,Topping,桌面耳放)
- 8千以上:HD 800 S(¥9,999,森海塞尔,声场王)、HE1000 Stealth(¥8,999,HIFIMAN,顶级声场)、U12t(¥16,999,64 Audio,舞台监听)、MEST MkII(¥12,800,qdc,混合单元)、Empyrean II(¥24,999,Meze,平板旗舰)、WA22(¥15,800,Woo Audio,胆机)
按使用场景
- 通勤:优先降噪和便携,WH-1000XM5(头戴降噪)、AirPods Pro 2(真无线降噪)、FreeBuds Pro 4(真无线降噪)、ER2SE(深入耳隔音)
- 家用 HiFi:优先音质和声场,HD 800 S(头戴动圈)、Empyrean II(头戴平板)、HE1000 Stealth(头戴平板)、DT 900 Pro X(监听)
- 便携出街:优先便携和手机直推,Aria 2(入耳动圈)、Timeless(入耳平板)、IE 900(入耳动圈旗舰)、U12t(入耳定制)
- 专业工作:优先准确和耐用,DT 900 Pro X(监听混音)、U12t(舞台监听)、KG770(监听入门)
- 折腾前端:M9701/RU7(便携解码)、A90 Discrete(桌面耳放)、WA22(胆机)、QDX(安卓系统解码)
按听音风格
- 古典:HD 800 S(声场王,乐器定位精确)、HE1000 Stealth(顶级声场,隐形磁体)、WA22(胆机,中频密度)
- 流行/电子:IE 900(单动圈,低频质感好)、Aria 2(入门,氛围感)、MEST MkII(混合单元,信息量爆炸)
- 人声/爵士:Empyrean II(平板,中频迷人)、WA22(胆机,人声爵士绝配)、U12t(舞台监听,人声突出)
- 监听/混音:DT 900 Pro X(真实不染色)、U12t(舞台监听)、ER2SE(直白调音)
- 降噪/通勤:WH-1000XM5(降噪标杆)、AirPods Pro 2(生态联动)、FreeBuds Pro 4(国产之光)
13.5 常见误区
- 唯参数论:频响曲线、单元数量、推力参数只是参考,听感才是最终标准。两根曲线几乎重合的塞子听感可能差很多(项目中"曲线玄学"帖子的讨论)
- 唯价格论:贵的不一定适合你,便宜的不一定差。Aria 2(¥799)在入门价位口碑极佳,而某些贵价设备可能调音不符合你的偏好
- 线材玄学:升级线可能有可闻差异,但盲听测试很多人听不出来。项目中"线材不可知论"帖子分享了盲听连败十次后挂闲鱼升级线的经历
- 前端优先:新手常犯的错误是先买贵前端再配塞子。正确顺序是先买好塞子,再考虑前端,最后才是线材(项目中"十年老烧"的人生经验)
- 煲机神话:煲机可能有轻微变化,但远没有商家宣传的那么神。不要被"煲机 500 小时才好听"的说法忽悠
十四、开发环境搭建
14.1 环境要求
DevEco Studio 5.0+(支持HarmonyOS 6.1.1)、HarmonyOS SDK API 24、Node.js 18+、至少8GB内存。
14.2 安装DevEco Studio

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

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

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

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

DevEco Studio提供实时预览功能,编辑器右侧打开Previewer面板,支持多设备尺寸预览、深色模式切换、组件树inspect等调试能力。
总结与展望
项目总结
AUDIO LAB 听感实验室是一个面向耳机发烧友的极客终端风评测社区应用,用 2837 行 ArkTS 代码完整实现了六大核心模块:
- 首页·评测流:16 条真实风格评测,今日精选置顶,写评测/详情/删除/分享 4 弹框
- 榜单·口碑榜:24 台设备排名,8 分类筛选,TOP3 领奖台,设备详情/价格提醒 2 弹框
- 对比·参数对比:双设备 7 维度横向对比,自动生成实验室结论,选择设备弹框
- 图鉴·设备库:24 台设备网格,10 品牌筛选,装备库管理,设备详情/加入装备库 2 弹框
- 圈子·烧友社区:12 条帖子,7 热门话题,发帖/详情回复 2 弹框
- 我的·个人中心:资料卡/数据统计/获赞趋势/装备库/设置,等级说明/勋章/关于 3 弹框
技术亮点:
- GitHub 深色模式配色,终端命令行风格,评分颜色编码系统
- 单文件架构,@Entry + @Component,@State 状态管理
- 14 个弹框集中式布尔变量管理,Stack 遮罩 + 内容卡片
- 3 处克制的属性动画(Tab 切换/星级点击/勋章缩放)
- 4 个纯函数工具层(分类图标/评分颜色/价格格式化/设备查找)
- 平板横屏自适应,layoutWeight + 百分比宽度
数据规模:
- 24 台设备(覆盖入耳/头戴/真无线/解码/耳放/胆机全品类)
- 16 条评测(认证作者 + 普通用户,技术流/体验流/玄学流多元视角)
- 12 条圈子帖子(求推荐/维修/理性讨论/玄学/退烧/经验)
- 7 个热门话题(12,840 帖求推荐到 2,890 帖二手交易)
设计哲学回顾
AUDIO LAB 的设计哲学可以用三个关键词概括:
克制:动画只有 3 处,颜色只有 4 种功能色,按钮只有 3 种样式。没有花哨的渐变、转场、粒子效果,一切服务于信息展示和交互效率。这符合终端美学的本质——终端的美不在于装饰,而在于信息的精准呈现。
数据驱动:评分用颜色编码,排名用条形图可视化,对比用胜负高亮,结论用模板自动生成。所有主观评价都被转化为可量化的数据,用户可以一眼看出设备档次、对比胜负、价格高低。这正是"用数据说话,让耳朵做主"的体现。
社区真实感:16 条评测和 12 条帖子的内容设计刻意避免了官方话术,而是模仿真实发烧友的语言风格——有技术分析,有主观感受,有玄学讨论,有退烧心得,有人生经验。“盲听测试连败十次之后挂闲鱼升级线”、“从3W桌面系统退回手机+小尾巴”、“先买好塞子,再考虑前端,最后才是线材”——这些内容让社区有了真实的温度。
未来展望
短期优化
- 组件拆分:将 2837 行单文件拆分为多文件结构,提升可维护性
- 搜索功能:实现真实的设备/评测/帖子搜索,支持关键词高亮
- 下拉刷新:评测流和帖子流增加下拉刷新交互
- 数据持久化:用户装备库、评测草稿、价格提醒等数据本地存储
中期扩展
- 用户系统:注册登录、个人主页、关注/粉丝、私信
- 内容创作:评测图片/视频上传、富文本编辑器、草稿箱
- 电商接入:设备购买链接、价格历史曲线、降价推送
- 数据可视化:设备雷达图、频响曲线对比、评分分布直方图
长期愿景
- AI 推荐:基于用户听音偏好和装备库,智能推荐设备和评测
- 社区治理:内容审核、举报系统、社区规范、优质内容激励
- 多端联动:手机/平板/车机/智慧屏多端同步,穿戴设备听音数据采集
- 开放平台:第三方设备数据接入、API 开放、开发者社区
用数据说话,让耳朵做主。
AUDIO LAB 听感实验室 v4.2.1 — 已收录 12,800 台设备 · 42,000 条评测 · 12 万烧友
本文基于 AUDIO LAB ArkTS 单文件源码深度解析,项目采用 HarmonyOS ArkTS 声明式 UI 框架,6 Tab + 14 弹框 + 3 组属性动画 + 4 个纯函数,完整实现极客终端风耳机评测社区核心业务。完整源码含 24 台设备 / 16 条评测 / 12 条帖子等全部数据,可按需获取。

完整代码
// ============================================================
// AUDIO LAB 听感实验室 —— 精简版核心源码
// 极客终端风:GitHub 深灰底 + 电光蓝 + 终端绿 + 琥珀警告色
// 完整版含 24 台设备 / 16 条评测 / 12 条帖子 / 14 弹框
// ============================================================
// ============ 类型定义 ============
interface DeviceItem {
id: number; name: string; brand: string; category: string
price: number; score: number; sound: number; comfort: number; build: number
battery: string; driver: string; tags: string[]; owned: boolean
}
interface ReviewItem {
id: number; user: string; avatar: string; device: string
rating: number; title: string; text: string
likes: number; comments: number; verified: boolean; time: string
}
interface PostItem {
id: number; user: string; avatar: string; topic: string
text: string; likes: number; replies: number; solved: boolean
}
interface ChartEntry { rank: number; deviceId: number; name: string; brand: string; score: number; price: number; trend: string }
interface TopicMeta { name: string; hot: number; posts: number }
// ============ 设备数据(精简版 12 台,完整版 24 台) ============
class DeviceItemData implements DeviceItem {
id: number = 0; name: string = ''; brand: string = ''; category: string = ''
price: number = 0; score: number = 0; sound: number = 0; comfort: number = 0; build: number = 0
battery: string = ''; driver: string = ''; tags: string[] = []; owned: boolean = false
constructor(id: number, name: string, brand: string, category: string, price: number,
score: number, sound: number, comfort: number, build: number, battery: string,
driver: string, tags: string[], owned: boolean) {
this.id = id; this.name = name; this.brand = brand; this.category = category
this.price = price; this.score = score; this.sound = sound; this.comfort = comfort
this.build = build; this.battery = battery; this.driver = driver; this.tags = tags; this.owned = owned
}
}
const mockDevices: DeviceItem[] = [
new DeviceItemData(1, 'HD 800 S', '森海塞尔', '头戴动圈', 9999, 9.6, 9.8, 8.2, 8.5, '有线', '38mm', ['.reference', '声场王'], false),
new DeviceItemData(2, 'Empyrean II', 'Meze', '头戴平板', 24999, 9.5, 9.4, 9.0, 8.8, '有线', '平板', ['旗舰', '易驱动'], false),
new DeviceItemData(3, 'U12t', '64 Audio', '入耳定制', 16999, 9.4, 9.5, 9.2, 8.6, '有线', '12单元', ['舞台监听', '均衡'], true),
new DeviceItemData(5, 'IE 900', '森海塞尔', '入耳动圈', 6999, 9.0, 9.2, 8.4, 9.0, '有线', '7mm', ['单动圈', '氛围感'], true),
new DeviceItemData(6, 'Aria 2', '水月雨', '入耳动圈', 799, 8.2, 8.4, 8.6, 7.8, '有线', '10mm', ['入门', '性价比'], true),
new DeviceItemData(8, 'WH-1000XM5', '索尼', '降噪头戴', 2499, 8.5, 8.2, 8.8, 9.4, '30小时', '30mm', ['降噪标杆', '通勤'], true),
new DeviceItemData(9, 'AirPods Pro 2', 'Apple', '真无线', 1899, 8.3, 7.8, 9.0, 8.8, '6小时', '动圈', ['生态联动', '空间音频'], true),
new DeviceItemData(11, 'DT 900 Pro X', '拜雅', '头戴开放', 3699, 8.9, 9.0, 8.6, 9.2, '有线', '45mm', ['监听', '混音'], false),
new DeviceItemData(13, 'HE1000 Stealth', 'HIFIMAN', '头戴平板', 8999, 9.3, 9.6, 8.5, 8.8, '有线', '平板', ['顶级声场', '古典'], false),
new DeviceItemData(15, 'RU7', '山灵', '便携解码', 1998, 8.5, 0, 0, 0, '14小时', 'R2R', ['R2R', '暖声'], true),
new DeviceItemData(19, 'WA22', 'Woo Audio', '胆机', 15800, 9.1, 0, 0, 0, '常供电', '三胆', ['胆味', '古典绝配'], false),
new DeviceItemData(22, 'Timeless', '七赫兹', '入耳平板', 1599, 8.6, 8.8, 8.0, 8.2, '有线', '14.6mm平板', ['千元平板', '一茬韭菜'], true),
// ... 完整版共 24 台(含 MEST MkII / Blessing 3 / FreeBuds Pro 4 / Ananda / M9701 / QDX / Zen Stream / A90 Discrete / Svanar Wireless / ER2SE / RS 220 / KG770)
]
// ============ 评测数据(精简版 8 条,完整版 16 条) ============
class ReviewItemData implements ReviewItem {
id: number = 0; user: string = ''; avatar: string = ''; device: string = ''
rating: number = 0; title: string = ''; text: string = ''
likes: number = 0; comments: number = 0; verified: boolean = false; time: string = ''
constructor(id: number, user: string, avatar: string, device: string, rating: number,
title: string, text: string, likes: number, comments: number, verified: boolean, time: string) {
this.id = id; this.user = user; this.avatar = avatar; this.device = device; this.rating = rating
this.title = title; this.text = text; this.likes = likes; this.comments = comments
this.verified = verified; this.time = time
}
}
const mockReviews: ReviewItem[] = [
new ReviewItemData(1, '频率响应', '🎧', 'HD 800 S', 5, '换上平衡线之后的新世界', '原线下的HD800S已经有帝王声场,换上官方平衡线后瞬态响应直接起飞,古典乐器的定位精确到米。建议搭配暖声前端中和高频锋利度。', 1284, 156, true, '2小时前'),
new ReviewItemData(2, '电波胖子', '🔋', 'WH-1000XM5', 5, '通勤党一年后的真实反馈', '降噪依然是TWS头戴标杆,地铁里世界瞬间安静。但夏天佩戴闷热扣一分舒适,头梁压力比上代小很多,长时间佩戴终于不夹头了。', 986, 203, true, '5小时前'),
new ReviewItemData(3, '麒麟臂练成中', '💪', 'Aria 2', 4, '百元价位还有对手吗', '水月雨刀法精准,799的价格给了千元级的解析,但人声稍微远了一点。烧友圈公认的入门第一站,闭眼买不亏。', 2104, 342, false, '8小时前'),
new ReviewItemData(4, '胆机收藏家', '🎛️', 'WA22', 5, '300B胆机的浪漫', '推平板虽然效率不高,但出来的中频密度是石机永远给不了的。夜深人静听人声爵士,胆管的微光就是仪式感本身。', 678, 89, true, '12小时前'),
new ReviewItemData(7, '老烧退烧药', '💊', 'IE 900', 5, '森海最后的动圈旗舰', '单动圈做到这个解析度,调音功力深不见底。低频量感偏多但质感极佳,听流行电子非常对味。', 763, 98, true, '2天前'),
new ReviewItemData(9, '声场永动机', '🌌', 'Empyrean II', 5, '两万五花得心服口服', '平板旗舰的声场是球形的,乐器从四面八方包裹过来。闭合式设计居然还有这种开扬感,罗马尼亚人的调音哲学太迷人了。', 942, 134, true, '3天前'),
new ReviewItemData(11, '混音师视角', '🎚️', 'DT 900 Pro X', 5, '干活耳朵的可靠伙伴', 'Stellar驱动器下潜扎实,中频真实不染色。混音八小时耳朵不累,这就是监听耳机的职业素养。', 689, 92, true, '4天前'),
new ReviewItemData(16, '仪式感研究员', '🕯️', 'HE1000 Stealth', 5, '隐形磁体技术不是噱头', '磁体隐形化后声场通透度肉眼可闻,嗯,可闻。大耳的尽头果然还是平板。', 1201, 167, true, '1周前'),
// ... 完整版共 16 条(含 Blessing 3 / Timeless / AirPods Pro 2 / MEST MkII / ER2SE / FreeBuds Pro 4 / A90 Discrete / RU7)
]
// ============ 圈子帖子(精简版 6 条,完整版 12 条) ============
class PostItemData implements PostItem {
id: number = 0; user: string = ''; avatar: string = ''; topic: string = ''
text: string = ''; likes: number = 0; replies: number = 0; solved: boolean = false
constructor(id: number, user: string, avatar: string, topic: string, text: string,
likes: number, replies: number, solved: boolean) {
this.id = id; this.user = user; this.avatar = avatar; this.topic = topic
this.text = text; this.likes = likes; this.replies = replies; this.solved = solved
}
}
const mockPosts: PostItem[] = [
new PostItemData(1, '新手小白', '🐣', '求推荐', '预算2000以内,主要听流行人声,在Aria 2和Timeless之间纠结,求大佬们指点!', 89, 67, true),
new PostItemData(3, '曲线玄学', '📉', '理性讨论', '频响曲线真的能代表听感吗?为什么两根曲线几乎重合的塞子听感差这么多?', 210, 189, false),
new PostItemData(6, '线材不可知论', '🧵', '玄学区', '盲听测试连败十次之后,我把3000块的升级线挂闲鱼了。理性讨论线材到底有没有用。', 356, 421, false),
new PostItemData(7, '毕业即退烧', '🎓', '退烧心得', '从3W的桌面系统退回手机+小尾巴,突然发现自己其实听不出那么大差距……', 289, 245, true),
new PostItemData(9, '低音狂魔', '🔊', '风格取向', '就喜欢动次打次轰头的感觉,有没有量感爆炸但糊得可爱的塞子推荐?', 78, 92, true),
new PostItemData(12, '十年老烧', '👴', '人生经验', '给年轻人一句话:先买好塞子,再考虑前端,最后才是线材。顺序反了就是无尽的折腾。', 421, 156, false),
// ... 完整版共 12 条(含 设备维修 / 通勤装备 / 古典专场 / 二手交易 / 学习场景 / 搭配求助)
]
// ============ 话题配置 ============
const TOPIC_CONFIG: TopicMeta[] = [
{ name: '求推荐', hot: 96, posts: 12840 }, { name: '设备维修', hot: 72, posts: 4320 },
{ name: '理性讨论', hot: 88, posts: 8920 }, { name: '通勤装备', hot: 81, posts: 6540 },
{ name: '古典专场', hot: 64, posts: 3210 }, { name: '玄学区', hot: 92, posts: 10240 },
{ name: '二手交易', hot: 58, posts: 2890 }
]
// ============ 工具函数(4 个纯函数) ============
function getCategoryIcon(category: string): string {
if (category.indexOf('入耳') >= 0) return '🎵'
if (category.indexOf('头戴') >= 0) return '🎧'
if (category.indexOf('解码') >= 0) return '🎚️'
if (category.indexOf('耳放') >= 0 || category.indexOf('胆机') >= 0) return '🎛️'
if (category.indexOf('串流') >= 0) return '📡'
if (category.indexOf('真无线') >= 0) return '📶'
return '🔊'
}
function getScoreColor(score: number): string {
if (score >= 9.2) return '#3FB950'
if (score >= 8.5) return '#58A6FF'
if (score >= 8.0) return '#D29922'
return '#8B949E'
}
function formatPrice(p: number): string { return '¥' + p.toLocaleString('en-US') }
function getDeviceById(id: number): DeviceItem { return mockDevices[id - 1] }
// ============ Tab 枚举 ============
enum LabTab { HOME, CHART, COMPARE, ATLAS, FORUM, ME }
// ============ 主入口 ============
@Entry
@Component
struct AudioLabApp {
@State activeTab: LabTab = LabTab.HOME
@Builder contentArea() {
Column() {
if (this.activeTab === LabTab.HOME) HomeContent()
else if (this.activeTab === LabTab.CHART) ChartContent()
else if (this.activeTab === LabTab.COMPARE) CompareContent()
else if (this.activeTab === LabTab.ATLAS) AtlasContent()
else if (this.activeTab === LabTab.FORUM) ForumContent()
else MeContent()
}.layoutWeight(1)
}
@Builder bottomTabItem(icon: string, label: string, tab: LabTab) {
Column() {
Text(icon).fontSize(19)
.opacity(this.activeTab === tab ? 1.0 : 0.4)
.scale({ x: this.activeTab === tab ? 1.16 : 1.0, y: this.activeTab === tab ? 1.16 : 1.0 })
.animation({ duration: 180, curve: Curve.EaseOut })
Text(label).fontSize(9)
.fontColor(this.activeTab === tab ? '#58A6FF' : '#6E7681')
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 })
.onClick(() => { this.activeTab = tab })
}
build() {
Column() {
this.contentArea()
Row() {
this.bottomTabItem('📡', '首页', LabTab.HOME)
this.bottomTabItem('🏆', '榜单', LabTab.CHART)
this.bottomTabItem('⚖️', '对比', LabTab.COMPARE)
this.bottomTabItem('🗂️', '图鉴', LabTab.ATLAS)
this.bottomTabItem('💬', '圈子', LabTab.FORUM)
this.bottomTabItem('👤', '我的', LabTab.ME)
}
.width('100%').backgroundColor('#161B22').padding({ top: 5, bottom: 7 })
.shadow({ radius: 14, color: '#66000000', offsetY: -4 })
}
.width('100%').height('100%').backgroundColor('#0D1117')
}
}
// ============ 终端式公共头部 ============
@Component
struct TerminalHeader {
title: string = '听感实验室'; subtitle: string = ''; gearCount: number = 4
onGearTap: () => void = () => {}; onSearchTap: () => void = () => {}
build() {
Column() {
Row() {
Column() {
Text('🎧 ' + this.gearCount + ' 台设备').fontSize(13).fontColor('#58A6FF').fontWeight(FontWeight.Medium)
Text('我的装备库').fontSize(8).fontColor('#6E7681').margin({ top: 1 })
}.alignItems(HorizontalAlign.Start).onClick(() => { this.onGearTap() })
Row() {
Text('🔍').fontSize(13).margin({ left: 10 })
Text('搜索设备 / 评测 / 帖子').fontSize(11).fontColor('#484F58').margin({ left: 6 })
}
.layoutWeight(1).height(34).backgroundColor('#0D1117')
.border({ width: 1, color: '#30363D' }).borderRadius(17).margin({ left: 12 })
.onClick(() => { this.onSearchTap() })
Text('⚙️').fontSize(16).width(34).height(34).textAlign(TextAlign.Center)
.backgroundColor('#21262D').borderRadius(17).margin({ left: 8 })
.onClick(() => { this.onGearTap() })
}.width('100%').padding({ left: 14, right: 14, top: 10 })
Row() {
Column() {
Text(this.title).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
if (this.subtitle !== '') Text(this.subtitle).fontSize(10).fontColor('#6E7681').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 10 })
}
.width('100%').backgroundColor('#161B22').border({ width: 1, color: '#21262D' })
}
}
// ============ Tab1:首页·评测流 ============
@Component
struct HomeContent {
@State showReviewDetail: boolean = false
@State showWriteModal: boolean = false
@State showDeleteModal: boolean = false
@State showShareModal: boolean = false
@State pickedReview: ReviewItem | null = null
@State ratingPicked: number = 5
@State reviewTitle: string = ''
@State reviewText: string = ''
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(1,4,9,0.78)').onClick(onClose)
}
// 弹框A:写评测(星级选择+标题+正文+同步圈子+取消/发布)
// 弹框B:删除确认(危险红主题,⚠️图标+警告说明+取消/删除)
// 弹框C:评测详情(底部抽屉,用户信息+全文+操作条+热门评论4条,constraintSize maxHeight 58%)
// 弹框D:分享(底部抽屉,6选项3列网格:微信/复制链接/生成长图/导出MD/收藏/举报)
// 完整版含 4 个弹框的完整实现
@Builder reviewCard(r: ReviewItem) {
Column() {
Row() {
Text(r.avatar).fontSize(22).width(40).height(40).textAlign(TextAlign.Center)
.backgroundColor('#21262D').borderRadius(20)
Column() {
Row() {
Text(r.user).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
if (r.verified) Text('✓ 认证').fontSize(8).fontColor('#3FB950')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).backgroundColor('#0D1117')
.borderRadius(7).margin({ left: 6 })
}
Text(r.time + ' · 评 ' + r.device).fontSize(9).fontColor('#6E7681').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
Text('★★★★★'.substring(0, r.rating)).fontSize(12).fontColor('#D29922')
}.width('100%')
Text(r.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#58A6FF').margin({ top: 10 }).width('100%')
Text(r.text).fontSize(11).fontColor('#C9D1D9').margin({ top: 6 }).width('100%')
.maxLines(3).textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('👍 ' + r.likes).fontSize(10).fontColor('#8B949E')
Text('💬 ' + r.comments).fontSize(10).fontColor('#8B949E').margin({ left: 16 })
Text('全文 ›').fontSize(10).fontColor('#58A6FF').layoutWeight(1).textAlign(TextAlign.End)
}.width('100%').margin({ top: 10 })
}
.width('100%').padding(14).backgroundColor('#161B22')
.border({ width: 1, color: '#21262D' }).borderRadius(16).margin({ bottom: 8 })
.onClick(() => { this.pickedReview = r; this.showReviewDetail = true })
}
build() {
Stack() {
Scroll() {
Column() {
TerminalHeader({ title: '听感实验室', subtitle: '今日新增评测 128 条 · 认证作者 34 人' })
// 今日精选大卡(🔥今日精选+编辑推荐,复用reviewCard展示mockReviews[8])
// 写评测入口(📝+分享你的听感…+写评测按钮,终端绿)
ForEach(mockReviews, (r: ReviewItem) => { this.reviewCard(r) }, (r: ReviewItem) => r.id.toString())
}.width('100%').padding({ bottom: 12 })
}.scrollBar(BarState.Off).layoutWeight(1)
// 4 个弹框条件渲染:if showWriteModal / showReviewDetail / showDeleteModal / showShareModal
}.width('100%').height('100%')
}
}
// ============ Tab2:榜单·口碑榜 ============
@Component
struct ChartContent {
@State pickedCat: string = '全部'
@State showDeviceModal: boolean = false
@State showPriceAlert: boolean = false
@State selectedDevice: DeviceItem | null = null
catList: string[] = ['全部', '入耳动圈', '入耳平板', '头戴平板', '头戴动圈', '真无线', '便携解码', '桌面耳放']
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(1,4,9,0.78)').onClick(onClose)
}
// 弹框E:设备详情(居中弹出,constraintSize maxHeight 78%,可滚动)
// 设备头图+综合分大号+三维评分条形图(声音/舒适/做工)+参数表(续航/换能/售价)
// +标签列表+近半年价格走势迷你柱(6根)+到手价提醒/加入对比按钮
// 弹框F:价格提醒(居中弹出,琥珀黄主题,🔔图标+3折扣快捷按钮+设置提醒)
build() {
Stack() {
Scroll() {
Column() {
TerminalHeader({ title: '口碑榜', subtitle: '基于 42,000+ 条真实评测加权计算' })
// 分类筛选横滑(8分类,选中电光蓝填充,未选中深灰)
// TOP3 领奖台(3列等宽,🥈🥇🥉,设备图标+名称+评分)
// 完整榜单(ForEach遍历24台,按分类过滤,每行排名+图标+名称/品牌/价格+评分条+评分数字)
ForEach(mockDevices, (d: DeviceItem, idx: number) => {
if (this.pickedCat === '全部' || d.category === this.pickedCat) {
Row() {
Text((idx + 1).toString()).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#D29922' : '#6E7681').width(30)
Text(getCategoryIcon(d.category)).fontSize(22).width(40).height(40)
.textAlign(TextAlign.Center).backgroundColor('#21262D').borderRadius(10)
Column() {
Text(d.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
Text(d.brand + ' · ' + formatPrice(d.price)).fontSize(9).fontColor('#6E7681').margin({ top: 2 })
Column() {
Column().width((d.score * 10) + '%').height(5)
.backgroundColor(getScoreColor(d.score)).borderRadius(3)
}.width('100%').height(5).backgroundColor('#21262D').borderRadius(3).margin({ top: 5 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
Text(d.score.toFixed(1)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(getScoreColor(d.score))
}
.width('100%').padding(12).backgroundColor('#161B22')
.border({ width: 1, color: '#21262D' }).borderRadius(14)
.margin({ top: 8, left: 12, right: 12 })
.onClick(() => { this.selectedDevice = d; this.showDeviceModal = true })
}
}, (d: DeviceItem) => d.id.toString() + this.pickedCat)
}.width('100%').padding({ bottom: 14 })
}.scrollBar(BarState.Off).layoutWeight(1)
// 2 个弹框条件渲染:if showDeviceModal / showPriceAlert
}.width('100%').height('100%')
}
}
// ============ Tab3:对比·参数对比 ============
@Component
struct CompareContent {
@State leftId: number = 1
@State rightId: number = 5
@State showPickModal: boolean = false
@State pickingSide: string = 'left'
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(1,4,9,0.78)').onClick(onClose)
}
// 弹框G:选择对比设备(居中弹出,可滚动列表,constraintSize maxHeight 62%,每行设备+选择按钮)
@Builder vsRow(label: string, leftVal: string, rightVal: string, highlight: string) {
Row() {
Text(leftVal).fontSize(11)
.fontWeight(highlight === 'left' ? FontWeight.Bold : FontWeight.Normal)
.fontColor(highlight === 'left' ? '#3FB950' : '#C9D1D9')
.layoutWeight(1).textAlign(TextAlign.End)
Text(label).fontSize(9).fontColor('#6E7681').width(64).textAlign(TextAlign.Center)
Text(rightVal).fontSize(11)
.fontWeight(highlight === 'right' ? FontWeight.Bold : FontWeight.Normal)
.fontColor(highlight === 'right' ? '#3FB950' : '#C9D1D9')
.layoutWeight(1).textAlign(TextAlign.Start)
}.width('100%').padding({ top: 10, bottom: 10 })
}
build() {
Stack() {
Scroll() {
Column() {
TerminalHeader({ title: '参数对比', subtitle: '选择两台设备进行横向对比' })
// VS 大卡(左右两列设备+中间VS危险红旋转-8°,点击更换打开选择弹框)
// 对比明细表(7行vsRow:综合评分/声音/舒适/做工/售价/换能方式/续航,Divider分隔)
Column() {
this.vsRow('综合评分', getDeviceById(this.leftId).score.toFixed(1), getDeviceById(this.rightId).score.toFixed(1),
getDeviceById(this.leftId).score > getDeviceById(this.rightId).score ? 'left' : 'right')
Divider().color('#21262D')
this.vsRow('声音', getDeviceById(this.leftId).sound.toFixed(1), getDeviceById(this.rightId).sound.toFixed(1),
getDeviceById(this.leftId).sound > getDeviceById(this.rightId).sound ? 'left' : 'right')
Divider().color('#21262D')
this.vsRow('舒适', getDeviceById(this.leftId).comfort.toFixed(1), getDeviceById(this.rightId).comfort.toFixed(1),
getDeviceById(this.leftId).comfort > getDeviceById(this.rightId).comfort ? 'left' : 'right')
Divider().color('#21262D')
this.vsRow('做工', getDeviceById(this.leftId).build.toFixed(1), getDeviceById(this.rightId).build.toFixed(1),
getDeviceById(this.leftId).build > getDeviceById(this.rightId).build ? 'left' : 'right')
Divider().color('#21262D')
this.vsRow('售价', formatPrice(getDeviceById(this.leftId).price), formatPrice(getDeviceById(this.rightId).price),
getDeviceById(this.leftId).price < getDeviceById(this.rightId).price ? 'left' : 'right')
Divider().color('#21262D')
this.vsRow('换能方式', getDeviceById(this.leftId).driver, getDeviceById(this.rightId).driver, 'none')
Divider().color('#21262D')
this.vsRow('续航/供电', getDeviceById(this.leftId).battery, getDeviceById(this.rightId).battery, 'none')
}
.width('100%').padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor('#161B22').border({ width: 1, color: '#21262D' }).borderRadius(18)
.margin({ top: 12, left: 12, right: 12 })
// 实验室结论(🤖标题+自动生成文案:综合来看{高分}更值得入手;若预算优先,选择{低价})
}.width('100%')
}.scrollBar(BarState.Off).layoutWeight(1)
// 1 个弹框条件渲染:if showPickModal
}.width('100%').height('100%')
}
}
// ============ Tab4:图鉴·设备库 ============
@Component
struct AtlasContent {
@State pickedBrand: string = '全部'
@State showDeviceModal: boolean = false
@State showOwnedModal: boolean = false
@State selectedDevice: DeviceItem | null = null
brandList: string[] = ['全部', '森海塞尔', 'HIFIMAN', '七赫兹', '水月雨', '索尼', 'Apple', '华为', '拜雅', '64 Audio']
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(1,4,9,0.78)').onClick(onClose)
}
// 弹框H:加入装备库确认(居中弹出,➕图标+说明+暂不/确认加入)
// 弹框I:图鉴版设备详情(居中弹出,设备大图100×100+名称+综合分+标签+加入装备库按钮)
build() {
Stack() {
Column() {
TerminalHeader({ title: '设备图鉴', subtitle: '收录 ' + mockDevices.length + ' 台 · 覆盖 32 个品牌' })
// 品牌筛选横滑(10品牌,选中电光蓝填充)
Scroll() {
Row() {
ForEach(this.brandList, (b: string) => {
Text(b).fontSize(11)
.fontColor(this.pickedBrand === b ? '#0D1117' : '#8B949E')
.fontWeight(this.pickedBrand === b ? FontWeight.Bold : FontWeight.Normal)
.padding({ left: 13, right: 13, top: 7, bottom: 7 })
.backgroundColor(this.pickedBrand === b ? '#58A6FF' : '#161B22')
.borderRadius(15).margin({ right: 8 })
.onClick(() => { this.pickedBrand = b })
}, (b: string) => b)
}.padding({ left: 14, right: 14 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
// 设备网格(FlexWrap,每卡45%宽度,设备图标+已有徽章+名称+品牌/分类+评分+价格)
Scroll() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(mockDevices, (d: DeviceItem) => {
if (this.pickedBrand === '全部' || d.brand === this.pickedBrand) {
Column() {
Row() {
Text(getCategoryIcon(d.category)).fontSize(28).width(56).height(56)
.textAlign(TextAlign.Center).backgroundColor('#21262D').borderRadius(14)
if (d.owned) Text('已有').fontSize(8).fontColor('#3FB950')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).backgroundColor('#0D1117').borderRadius(7)
}.justifyContent(FlexAlign.SpaceBetween).width('100%')
Text(d.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
.margin({ top: 8 }).maxLines(1).width('100%')
Text(d.brand + ' · ' + d.category).fontSize(9).fontColor('#6E7681').margin({ top: 2 })
Row() {
Text(d.score.toFixed(1)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(getScoreColor(d.score))
Text(formatPrice(d.price)).fontSize(9).fontColor('#8B949E').margin({ left: 8 })
}.margin({ top: 5 })
}
.width('45%').alignItems(HorizontalAlign.Start).padding(12)
.backgroundColor('#161B22').border({ width: 1, color: '#21262D' })
.borderRadius(16).margin(6)
.onClick(() => { this.selectedDevice = d; this.showDeviceModal = true })
}
}, (d: DeviceItem) => d.id.toString() + this.pickedBrand)
}.width('100%').padding({ left: 8, right: 8, top: 10, bottom: 14 })
}.scrollBar(BarState.Off).layoutWeight(1)
}.width('100%').height('100%')
// 2 个弹框条件渲染:if showDeviceModal / showOwnedModal
}.width('100%').height('100%')
}
}
// ============ Tab5:圈子·烧友社区 ============
@Component
struct ForumContent {
@State pickedTopic: string = '全部'
@State showPostModal: boolean = false
@State showAskModal: boolean = false
@State showReplyModal: boolean = false
@State askTitle: string = ''
@State askBody: string = ''
@State replyText: string = ''
@State pickedPost: PostItem | null = null
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(1,4,9,0.78)').onClick(onClose)
}
// 弹框J:发帖提问(居中弹出,❓标题+标题输入+正文输入+话题选择+取消/发布)
// 弹框K:帖子详情(底部抽屉,#话题#+用户信息+已解决徽章+全文+全部回复5条+底部输入条,constraintSize maxHeight 55%)
build() {
Stack() {
Scroll() {
Column() {
TerminalHeader({ title: '烧友圈子', subtitle: '今日新帖 342 条 · 采纳率 78%',
onGearTap: () => { this.showAskModal = true } })
// 热门话题横滑(7话题,#话题名#+帖子数,点击切换筛选,再次点击取消)
// 发帖入口(❓+有问题?向12万烧友提问…+提问按钮终端绿)
// 帖子流(ForEach遍历12条,按话题过滤,每条头像+用户名+话题标签+已解决徽章+正文3行+点赞/回复/查看全部)
ForEach(mockPosts, (p: PostItem) => {
if (this.pickedTopic === '全部' || p.topic === this.pickedTopic) {
Column() {
Row() {
Text(p.avatar).fontSize(20).width(36).height(36).textAlign(TextAlign.Center)
.backgroundColor('#21262D').borderRadius(18)
Column() {
Text(p.user).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
Text('#' + p.topic + '#').fontSize(9).fontColor('#58A6FF').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
if (p.solved) Text('已解决 ✓').fontSize(9).fontColor('#3FB950')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor('#0D1117').borderRadius(8)
}.width('100%')
Text(p.text).fontSize(12).fontColor('#C9D1D9').margin({ top: 8 }).width('100%')
.maxLines(3).textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('👍 ' + p.likes).fontSize(10).fontColor('#8B949E')
Text('💬 ' + p.replies).fontSize(10).fontColor('#8B949E').margin({ left: 16 })
Text('查看全部 ›').fontSize(10).fontColor('#58A6FF').layoutWeight(1).textAlign(TextAlign.End)
}.width('100%').margin({ top: 10 })
}
.width('100%').padding(13).backgroundColor('#161B22')
.border({ width: 1, color: '#21262D' }).borderRadius(16)
.margin({ top: 8, left: 12, right: 12 })
.onClick(() => { this.pickedPost = p; this.showPostModal = true })
}
}, (p: PostItem) => p.id.toString() + this.pickedTopic)
}.width('100%').padding({ bottom: 14 })
}.scrollBar(BarState.Off).layoutWeight(1)
// 2 个弹框条件渲染:if showAskModal / showPostModal
}.width('100%').height('100%')
}
}
// ============ Tab6:我的·个人中心 ============
@Component
struct MeContent {
@State showBadgeModal: boolean = false
@State showLevelModal: boolean = false
@State showAboutModal: boolean = false
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(1,4,9,0.78)').onClick(onClose)
}
// 弹框L:等级说明(居中弹出,终端风格,$ whoami --level等宽字体+6级等级列表+关闭)
// 弹框M:勋章(居中弹出,12勋章4列网格,已解锁1.06倍缩放+未解锁20%透明度,关闭)
// 弹框N:关于(居中弹出,🎚️图标+应用名+版本号+简介+链接+关闭)
build() {
Stack() {
Scroll() {
Column() {
// 资料卡(头像64×64+用户名@freq_response+Lv.4实验室常驻·认证作者+设备库4台·徽章5枚+ℹ️按钮)
// 数据统计(4列:评测34电光蓝/获赞2861终端绿/粉丝342琥珀黄/收藏12危险红)
// 近30天获赞趋势(10根柱状图,≥40变绿,底部对齐)
// 我的装备库(标题+ForEach过滤owned设备,4台:U12t/IE900/Aria2/WH-1000XM5,每行图标+名称+品牌/价格+已购徽章)
Text('🎧 我的装备库').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
.width('100%').padding({ left: 16, top: 16 })
ForEach(mockDevices, (d: DeviceItem) => {
if (d.owned) {
Row() {
Text(getCategoryIcon(d.category)).fontSize(22).width(42).height(42)
.textAlign(TextAlign.Center).backgroundColor('#21262D').borderRadius(11)
Column() {
Text(d.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#F0F6FC')
Text(d.brand + ' · ' + formatPrice(d.price)).fontSize(9).fontColor('#6E7681').margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
Text('已购').fontSize(9).fontColor('#3FB950')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor('#0D1117').borderRadius(8)
}
.width('100%').padding(12).backgroundColor('#161B22')
.border({ width: 1, color: '#21262D' }).borderRadius(14)
.margin({ top: 6, left: 12, right: 12 })
}
}, (d: DeviceItem) => 'own' + d.id.toString())
// 设置列表(3项:我的徽章5/12→勋章弹框 / 价格提醒设置3个生效中 / 关于实验室v4.2.1→关于弹框,Divider分隔)
Column() {
Row() {
Text('🏅').fontSize(16)
Text('我的徽章').fontSize(13).fontColor('#F0F6FC').margin({ left: 12 }).layoutWeight(1)
Text('5/12').fontSize(10).fontColor('#6E7681')
Text('›').fontSize(13).fontColor('#484F58').margin({ left: 8 })
}.width('100%').padding({ top: 15, bottom: 15 }).onClick(() => { this.showBadgeModal = true })
Divider().color('#21262D')
Row() {
Text('🔔').fontSize(16)
Text('价格提醒设置').fontSize(13).fontColor('#F0F6FC').margin({ left: 12 }).layoutWeight(1)
Text('3 个生效中').fontSize(10).fontColor('#D29922')
Text('›').fontSize(13).fontColor('#484F58').margin({ left: 8 })
}.width('100%').padding({ top: 15, bottom: 15 })
Divider().color('#21262D')
Row() {
Text('ℹ️').fontSize(16)
Text('关于实验室').fontSize(13).fontColor('#F0F6FC').margin({ left: 12 }).layoutWeight(1)
Text('v4.2.1').fontSize(10).fontColor('#6E7681')
Text('›').fontSize(13).fontColor('#484F58').margin({ left: 8 })
}.width('100%').padding({ top: 15, bottom: 15 }).onClick(() => { this.showAboutModal = true })
}
.width('100%').padding({ left: 16, right: 16 })
.backgroundColor('#161B22').border({ width: 1, color: '#21262D' }).borderRadius(18)
.margin({ top: 16, left: 12, right: 12, bottom: 16 })
}.width('100%')
}.scrollBar(BarState.Off).layoutWeight(1)
// 3 个弹框条件渲染:if showBadgeModal / showLevelModal / showAboutModal
}.width('100%').height('100%')
}
}
更多推荐




所有评论(0)