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 设计理念:终端即实验室

整个应用的视觉语言可以概括为"终端即实验室":

  1. 命令行式标题:页面标题使用大号粗体白字,副标题用灰小字,模拟终端输出的层次感
  2. 数据优先:评分、价格、参数以等宽数字呈现,颜色编码(绿=优秀、蓝=良好、黄=及格、灰=一般)
  3. 卡片即窗口:每个内容卡片都有 1px 边框和圆角,像终端里的独立窗口
  4. 操作即命令:按钮使用纯色填充(绿=确认、蓝=主操作、红=危险),文字简洁如命令
  5. 状态即标签:已解决、已认证、已购等状态用小标签呈现,像 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 终端风格细节

  1. 等宽字体:等级说明弹框中的 $ whoami --level 命令使用 fontFamily('monospace'),模拟终端命令行
  2. 命令式按钮:写评测、发布、确认加入等按钮使用纯色填充 + 圆角胶囊,像终端里的可执行命令
  3. 标签系统#求推荐##玄学区# 等话题标签使用 # 包裹,像社交媒体的 hashtag,也像代码注释
  4. 状态徽章✓ 认证已解决 ✓已购 等小徽章使用深色背景 + 彩色文字,像 Git 的 status check
  5. 数据可视化:评分条使用彩色填充 + 深色背景,像终端里的进度条;获赞趋势柱状图使用蓝绿双色,高于 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 组件间通信模式

项目采用单向数据流 + 回调函数的通信模式:

  1. 父→子:通过组件属性传递数据,如 TerminalHeader({ title: '听感实验室', subtitle: '...' })
  2. 子→父:通过回调函数传递事件,如 onGearTap: () => voidonSearchTap: () => void
  3. 兄弟组件:通过父组件的 @State 中转,如对比页中 leftIdrightId 的更新
  4. 弹框数据:通过 @State 变量传递,如 selectedDevice: DeviceItem | nullpickedReview: ReviewItem | null

3.4 弹框管理模式

14 个弹框全部采用集中式布尔变量管理

  • 每个 Tab 组件内部维护自己的弹框显隐状态(如 showWriteModalshowReviewDetail
  • 弹框通过 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

  • AudioLabAppactiveTab: LabTab —— 当前激活的 Tab
  • HomeContent:4 个弹框显隐 + pickedReview + ratingPicked + reviewTitle + reviewText
  • ChartContentpickedCat + 2 个弹框显隐 + selectedDevice
  • CompareContentleftId + rightId + showPickModal + pickingSide
  • AtlasContentpickedBrand + 2 个弹框显隐 + selectedDevice
  • ForumContentpickedTopic + 3 个弹框显隐 + askTitle + askBody + replyText + pickedPost
  • MeContent:3 个弹框显隐

这种组件级状态管理的优点是简单直接,缺点是跨 Tab 数据共享需要通过全局常量(如 mockDevices)或重新查询实现。

4.4 工具函数层

4 个纯函数构成了项目的工具层:

  1. getCategoryIcon(category):根据分类名称返回 emoji 图标,入耳→🎵、头戴→🎧、解码→🎚️、耳放/胆机→🎛️、串流→📡、真无线→📶、其他→🔊
  2. getScoreColor(score):评分颜色编码,详见第二章
  3. formatPrice§:价格格式化,¥ + 千分位(toLocaleString('en-US')),如 ¥9,999
  4. 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 个)
  1. 写评测弹框(writeModal):居中弹出,包含星级选择(5 星点击,选中星放大动画)、评测标题输入、详细听感输入、同步到圈子开关、取消/发布按钮
  2. 评测详情弹框(reviewDetailModal):底部抽屉式,顶部圆角 24,包含完整评测内容、操作条(点赞/评论/分享/删除)、热门评论列表(4 条模拟评论)、constraintSize({ maxHeight: '58%' }) 限高
  3. 删除确认弹框(deleteModal):居中弹出,危险红主题,⚠️ 图标 + 删除确认文案 + 取消/删除按钮
  4. 分享弹框(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 个)
  1. 设备详情弹框(deviceModal):居中弹出,constraintSize({ maxHeight: '78%' }) 限高,可滚动。包含设备图标(62×62)+ 设备名 + 品牌/分类/换能方式 + 价格 + 综合分大号(颜色编码)+ 三维评分条形图(声音/舒适/做工,不同颜色)+ 参数表(续航/换能方式/售价,Divider 分隔)+ 标签列表 + 近半年价格走势迷你柱状图(6 根,≤88 变绿)+ 到手价提醒/加入对比按钮
  2. 价格提醒弹框(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 组件:
    1. 综合评分(高分者终端绿加粗)
    2. 声音(高分者绿加粗)
    3. 舒适(高分者绿加粗)
    4. 做工(高分者绿加粗)
    5. 售价(低价者绿加粗)
    6. 换能方式(无高亮,纯文本)
    7. 续航/供电(无高亮,纯文本)
  • vsRow 布局:左值(右对齐)+ 标签(居中,64vp 宽)+ 右值(左对齐)
  • 每行之间用 Divider().color('#21262D') 分隔
  • 高亮逻辑通过 highlight 参数控制,传入 'left'/'right'/'none'
实验室结论
  • 🤖 实验室结论 标题(终端绿)
  • 自动生成文案:综合来看 {高分设备} 更值得入手;若预算优先,选择 {低价设备}
  • 文案通过模板字符串动态生成,getDeviceById(this.leftId).score > getDeviceById(this.rightId).score 判断高分设备
对比弹框(1 个)

选择对比设备弹框(pickModal):居中弹出,标题显示选择左侧还是右侧,可滚动设备列表(constraintSize({ maxHeight: '62%' })),每行设备图标 + 名称/品牌/价格 + 选择 按钮,点击后更新 leftIdrightId 并关闭弹框。

对比·参数对比

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 个)
  1. 发帖提问弹框(askModal):居中弹出,❓ 发起提问 标题 + 标题输入 + 正文输入 + 话题选择(默认求推荐,可更换)+ 取消/发布按钮
  2. 帖子详情弹框(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 项设置,每项:图标 + 名称 + 右侧状态/版本 + 箭头
    1. 我的徽章(5/12)→ 打开勋章弹框
    2. 价格提醒设置(3 个生效中,琥珀黄)→ 无弹框(占位)
    3. 关于实验室(v4.2.1)→ 打开关于弹框
  • 每项之间用 Divider().color('#21262D') 分隔
我的弹框(3 个)
  1. 等级说明弹框(levelModal):居中弹出,终端风格,$ whoami --level 命令(等宽字体,终端绿)+ 当前等级 + 6 级等级列表(Lv.1 耳塞新手 → Lv.6 首席研究员),每行:等级编号(电光蓝,等宽)+ 等级名 + 升级条件 + 关闭按钮
  2. 勋章弹框(badgeModal):居中弹出,🏅 我的勋章 标题 + 5/12 已解锁 + 12 个勋章网格(4 列),已解锁勋章正常显示 + 缩放动画,未解锁勋章 20% 透明度 + 缩小,底部关闭按钮
  3. 关于弹框(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 个)
  1. 写评测弹框(writeModal)

    • 星级选择:5 颗星,点击设置 ratingPicked,选中星放大 1.25 倍动画
    • 评测标题:TextInput,单行
    • 详细听感:TextInput,多行(64vp 高)
    • 同步到圈子:已开启开关(展示用,无实际交互)
    • 操作:取消(深灰)+ 发布(终端绿)
  2. 评测详情弹框(reviewDetailModal)

    • 底部抽屉,constraintSize({ maxHeight: '58%' })
    • 标题栏:评测详情 + ❌ 关闭
    • 用户信息:头像 + 用户名 + 认证徽章 + 时间/设备 + 星级
    • 评测标题 + 全文
    • 操作条:👍 点赞 + 💬 评论 + ↗ 分享 + 🗑 删除
    • 热门评论:4 条模拟评论,每条头像 + 用户名 + 评论文本 + 👍
  3. 删除确认弹框(deleteModal)

    • 居中弹出,危险红主题
    • ⚠️ 大号图标 + 删除这条评测?(危险红粗体)
    • 警告说明:删除后点赞与评论数据将一并清除,且无法恢复
    • 操作:取消(深灰)+ 删除(危险红填充)
  4. 分享弹框(shareModal)

    • 底部抽屉
    • 6 个分享选项,3 列 FlexWrap 网格:微信💬、复制链接🔗、生成长图📜、导出MD📤、收藏⭐、举报🚩
    • 每个选项:圆形图标(48×48)+ 标签文字
    • 取消按钮
榜单弹框(2 个)
  1. 设备详情弹框(deviceModal)

    • 居中弹出,constraintSize({ maxHeight: '78%' }),可滚动
    • 设备头图:图标(62×62)+ 设备名 + 品牌/分类/换能方式 + 价格 + ❌ 关闭
    • 综合分 + 三维评分条形图:
      • 声音:电光蓝条,宽度 = sound×10%
      • 舒适:终端绿条,宽度 = comfort×10%
      • 做工:琥珀黄条,宽度 = build×10%
    • 参数表:续航/供电、换能方式、参考售价,Divider 分隔
    • 标签列表:#标签 横向排列
    • 近半年价格走势:6 根迷你柱状图,≤88 变绿(表示降价)
    • 操作:🔔 到手价提醒(深灰)+ 加入对比(电光蓝)
  2. 价格提醒弹框(priceAlertModal)

    • 居中弹出,琥珀黄主题
    • 🔔 图标 + 到手价提醒 标题 + 说明文案
    • 3 个折扣快捷按钮:9.5折、9折、8.5折(深灰背景,琥珀黄文字)
    • 设置提醒按钮(终端绿)
对比弹框(1 个)
  1. 选择对比设备弹框(pickModal)
    • 居中弹出,constraintSize({ maxHeight: '62%' }),可滚动
    • 标题:选择设备(左侧/右侧),根据 pickingSide 动态显示
    • 设备列表:24 台设备,每行图标 + 名称/品牌/价格 + 选择按钮
    • 点击选择后更新 leftIdrightId 并关闭
图鉴弹框(2 个)
  1. 图鉴版设备详情弹框(deviceModal)

    • 与榜单页设备详情同名但实现不同,更简洁
    • 设备大图(100×100)+ 设备名 + 品牌/分类 + 综合分 + 价格
    • 标签列表
    • 装备库按钮:未拥有显示 + 加入装备库(电光蓝填充),已拥有显示 已在装备库 ✓(深灰背景绿字)
    • 点击加入装备库打开确认弹框
  2. 加入装备库确认弹框(ownedModal)

    • 居中弹出
    • ➕ 图标 + 加入我的装备库 标题 + 说明文案(加入后可记录购入价与使用时长,并同步到圈子个人页)
    • 操作:暂不(深灰)+ 确认加入(终端绿)
圈子弹框(2 个)
  1. 发帖提问弹框(askModal)

    • 居中弹出
    • 发起提问 标题
    • 标题输入:TextInput,单行
    • 正文输入:TextInput,多行(56vp 高)
    • 话题选择:话题:求推荐 + 更换 ›(展示用)
    • 操作:取消(深灰)+ 发布(终端绿)
  2. 帖子详情弹框(postModal)

    • 底部抽屉,constraintSize({ maxHeight: '55%' })
    • 标题栏:#话题#(电光蓝粗体)+ ❌ 关闭
    • 用户信息:头像 + 用户名 + 已解决徽章
    • 帖子全文
    • 全部回复:5 条模拟回复,每条头像 + 用户名 + 回复文本 + 👍
    • 底部输入条:回复输入框 + 发送按钮(终端绿)
我的弹框(3 个)
  1. 等级说明弹框(levelModal)

    • 居中弹出,终端风格
    • $ whoami --level 命令(等宽字体,终端绿)
    • 当前等级:Lv.4 实验室常驻(大号粗体)
    • 6 级等级列表,每行:等级编号(电光蓝,等宽字体,44vp 宽)+ 等级名 + 升级条件
      • Lv.1 耳塞新手:注册即达
      • Lv.2 数据熟手:发布 3 条评测
      • Lv.3 听感老鸟:获得 50 赞
      • Lv.4 实验室常驻:获得 500 赞(当前)
      • Lv.5 金耳朵认证:评测被官方精选
      • Lv.6 首席研究员:年度TOP作者
    • 关闭按钮(深灰背景,电光蓝文字)
  2. 勋章弹框(badgeModal)

    • 居中弹出
    • 🏅 我的勋章 标题 + 5 / 12 已解锁
    • 12 个勋章网格,4 列 FlexWrap:
      • 已解锁(5 个):正常显示 + 1.06 倍缩放动画
      • 未解锁(7 个):20% 透明度 + 0.94 倍缩小
    • 关闭按钮
  3. 关于弹框(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 交互反馈的其他方式

除了属性动画,项目还通过以下方式提供交互反馈:

  1. 颜色变化:选中态使用填充背景(电光蓝/终端绿),未选中使用深灰卡片
  2. 不透明度变化:Tab 图标激活 1.0 / 未激活 0.4,勋章已解锁 1.0 / 未解锁 0.2
  3. 边框变化:选中态可能有边框高亮(本项目主要用背景色区分)
  4. 文字样式:激活 Tab 标签粗体 + 电光蓝,未激活常规 + 灰
  5. 指示条:底部 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 数据的小瑕疵,实际应用中应通过排序函数动态排序。

榜单展示层次
  1. TOP3 领奖台:3 列等宽,中间冠军🥇,左右亚军🥈和季军🥉,展示奖牌 + 设备图标 + 名称 + 评分
  2. 完整榜单:24 台设备逐条列出,每行排名 + 图标 + 名称/品牌/价格 + 评分条 + 评分数字
  3. 分类筛选: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 项对比维度分为三类:

  1. 评分维度(有胜负):综合评分、声音、舒适、做工——高分者终端绿加粗
  2. 价格维度(有胜负):售价——低价者终端绿加粗
  3. 参数维度(无胜负):换能方式、续航/供电——纯文本展示,无高亮
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 标记左侧/右侧
  • 选择设备后更新 leftIdrightId,对比明细和结论自动刷新
  • 通过 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 项设置,每项:图标 + 名称 + 右侧状态/版本 + 箭头:

  1. 我的徽章:右侧 5/12 → 打开勋章弹框
  2. 价格提醒设置:右侧 3 个生效中(琥珀黄)→ 无弹框(占位)
  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 可优化点

数据层面
  1. 大列表虚拟化:当前 24 台设备、16 条评测、12 条帖子数据量小,无需虚拟化。若扩展到数百条,应使用 LazyForEach 实现懒加载
  2. 图片资源:当前使用 emoji 作为设备图标和用户头像,零网络请求。若替换为真实图片,应使用 Imagealtoffset 等属性优化加载体验
  3. 数据缓存:当前所有数据为内存常量,重启后丢失。可接入 Preferences 或数据库持久化用户装备库、评测草稿等
组件层面
  1. 组件拆分:当前单文件 2837 行,可按功能拆分为多个文件(如 components/models/utils/pages/),提升可维护性
  2. 自定义组件复用reviewCardvsRowmodalOverlay 等已通过 @Builder 复用,可进一步抽取为独立 @Component 组件
  3. 状态管理:当前使用组件级 @State,跨 Tab 数据共享不便。可引入 AppStorageProvide/Inject 实现全局状态管理
交互层面
  1. 下拉刷新:评测流、帖子流可增加下拉刷新,模拟数据更新
  2. 无限滚动:当前数据量固定,可扩展为分页加载
  3. 手势操作:可增加左滑删除、长按菜单等手势交互
  4. 搜索功能:当前搜索框为展示用,可实现真实的设备/评测/帖子搜索

12.3 扩展方向

功能扩展
  1. 设备对比增强:支持 3 台以上设备对比、雷达图可视化、历史价格曲线
  2. 评测系统增强:评测维度评分(声音/舒适/做工/性价比)、评测图片上传、评测视频
  3. 社区增强:关注系统、私信、通知推送、社区等级与勋章联动
  4. 电商接入:设备购买链接、价格历史追踪、降价提醒推送
  5. 个人数据:听音时长统计、设备使用频率、听感偏好分析
技术扩展
  1. 多端适配:当前已适配平板横屏,可进一步适配折叠屏、车机、智慧屏
  2. 国际化:当前为中文界面,可扩展多语言支持
  3. 深色/浅色模式:当前为固定深色模式,可支持系统主题切换
  4. 无障碍:增加屏幕阅读器支持、颜色对比度优化、字体大小适配
  5. 性能监控:接入性能监控 SDK,跟踪页面加载时间、帧率、内存占用
数据扩展
  1. 真实数据接入:当前为 Mock 数据,可接入后端 API 获取真实设备库、评测、社区数据
  2. 用户生成内容:支持用户发布评测、帖子、回复,数据持久化到后端
  3. 设备数据库:扩展到数百台设备,支持按品牌、分类、价格、评分多维度筛选
  4. 评测数据聚合:基于用户评测计算设备综合评分、维度评分,生成口碑榜

第十三章 音频评测知识与选购指南

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 常见误区

  1. 唯参数论:频响曲线、单元数量、推力参数只是参考,听感才是最终标准。两根曲线几乎重合的塞子听感可能差很多(项目中"曲线玄学"帖子的讨论)
  2. 唯价格论:贵的不一定适合你,便宜的不一定差。Aria 2(¥799)在入门价位口碑极佳,而某些贵价设备可能调音不符合你的偏好
  3. 线材玄学:升级线可能有可闻差异,但盲听测试很多人听不出来。项目中"线材不可知论"帖子分享了盲听连败十次后挂闲鱼升级线的经历
  4. 前端优先:新手常犯的错误是先买贵前端再配塞子。正确顺序是先买好塞子,再考虑前端,最后才是线材(项目中"十年老烧"的人生经验)
  5. 煲机神话:煲机可能有轻微变化,但远没有商家宣传的那么神。不要被"煲机 500 小时才好听"的说法忽悠

十四、开发环境搭建

14.1 环境要求

DevEco Studio 5.0+(支持HarmonyOS 6.1.1)、HarmonyOS SDK API 24、Node.js 18+、至少8GB内存。

14.2 安装DevEco Studio

DevEco Studio 安装界面

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

14.3 配置SDK

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 代码完整实现了六大核心模块:

  1. 首页·评测流:16 条真实风格评测,今日精选置顶,写评测/详情/删除/分享 4 弹框
  2. 榜单·口碑榜:24 台设备排名,8 分类筛选,TOP3 领奖台,设备详情/价格提醒 2 弹框
  3. 对比·参数对比:双设备 7 维度横向对比,自动生成实验室结论,选择设备弹框
  4. 图鉴·设备库:24 台设备网格,10 品牌筛选,装备库管理,设备详情/加入装备库 2 弹框
  5. 圈子·烧友社区:12 条帖子,7 热门话题,发帖/详情回复 2 弹框
  6. 我的·个人中心:资料卡/数据统计/获赞趋势/装备库/设置,等级说明/勋章/关于 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桌面系统退回手机+小尾巴”、“先买好塞子,再考虑前端,最后才是线材”——这些内容让社区有了真实的温度。

未来展望

短期优化
  1. 组件拆分:将 2837 行单文件拆分为多文件结构,提升可维护性
  2. 搜索功能:实现真实的设备/评测/帖子搜索,支持关键词高亮
  3. 下拉刷新:评测流和帖子流增加下拉刷新交互
  4. 数据持久化:用户装备库、评测草稿、价格提醒等数据本地存储
中期扩展
  1. 用户系统:注册登录、个人主页、关注/粉丝、私信
  2. 内容创作:评测图片/视频上传、富文本编辑器、草稿箱
  3. 电商接入:设备购买链接、价格历史曲线、降价推送
  4. 数据可视化:设备雷达图、频响曲线对比、评分分布直方图
长期愿景
  1. AI 推荐:基于用户听音偏好和装备库,智能推荐设备和评测
  2. 社区治理:内容审核、举报系统、社区规范、优质内容激励
  3. 多端联动:手机/平板/车机/智慧屏多端同步,穿戴设备听音数据采集
  4. 开放平台:第三方设备数据接入、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%')
  }
}
Logo

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

更多推荐