本文基于 HarmonyOS NEXT(API 24)ArkTS 声明式 UI 框架,完整拆解一个乐高二手交易与交换社区应用「QQ乐高·积木回血社」的源码实现。应用以乐高蓝 #0057A7 为主色、亮黄 #FFD400 为强调色、乐高红 #E8483C 为危险色,构建了底部 4 Tab(集市/交换/图鉴/我的)+ 顶部 6 Tab(全城/绝版/图纸/人仔/科技/街景)的双层导航体系,配套 14 个乐高积木粒子的水平向右飘动动画(🧱🧩✨🟨🟦 五图标循环,模拟积木在传送带上向右移动、乐高工厂出件口一块接一块送出的场景)、绝版 Tab 绝版稀有度柱状图(375 骑士城堡 100 分红高亮登顶)、图纸 Tab MOC 搭建时间轴(亮黄竖线连接 + 步骤列表带亮黄圆形序号)、科技 Tab 旗舰超跑对决卡 + 侧栏统计、街景 Tab 收藏标签云 + 集齐街景三杰彩蛋、4 个模态弹窗(发布出售新增/发起交换新增/下架删除/套装详情查询),覆盖从乐高闲置出售、以物换物、MOC 图纸分享、图鉴收藏进度到个人砖友成就的完整乐高二手交易闭环。

QQ乐高·积木回血社整体运行效果

一、引言:出闲置·收绝版·换心头好

乐高是成年人的童年解药——一盒绝版街景能让你回到童年拼搭的快乐,一个初版白兵人仔是星战迷的信仰,一份 MOC 图纸能让你用现有零件搭出属于自己的世界。但乐高圈也有自己的烦恼:拼完的套装占地方、绝版套装价格飞涨买不到、人仔重复了想换别的、MOC 图纸散落各处难找。「QQ乐高·积木回血社」正是为这群乐高爱好者打造的移动端二手交易与交换社区:用户可以在集市按类型(全城/绝版/图纸/人仔/科技/街景)发现乐高好物、在交换页以物换物、在图鉴页记录收藏进度、在个人中心追踪在售套装与累计成交额。

应用的视觉语言高度统一:乐高蓝代表乐高品牌的经典与信任,亮黄代表乐高积木的明亮与快乐,乐高红代表绝版的稀缺与交易的热度。整个应用采用明快乐高风——页面底色 #F4F8FF 浅蓝、卡片纯白、头部三色渐变 #0057A7→#1E88E5→#42A5F5(从乐高蓝到亮蓝到浅蓝,模拟乐高积木从深蓝到浅蓝的渐变层次)。粒子系统选用乐高砖 🧱、拼图 🧩、闪光 ✨、黄块 🟨、蓝块 🟦 五种图标循环,以所有应用中最快的速度(speed 0.6~2.0,setInterval 80ms)水平向右飘动——这一设计模拟了积木在传送带上向右移动、乐高工厂出件口一块接一块送出的场景,与垂直飘动的常规粒子形成鲜明反差,是所有应用中唯二采用水平飘动粒子的应用(与奶茶应用并列,但图标更贴合乐高主题)。

本文将从架构概览、色彩系统、数据模型、工具函数与筛选函数、粒子动画系统、双层导航、6 大集市差异化布局、交换/图鉴/我的三大 Tab、4 个模态弹窗、特性对比、完整代码、开发环境搭建等维度,对源码 1097.ets 进行逐模块深度解析。

二、架构概览:单文件声明式组件 + 状态驱动

整个应用采用 ArkTS 声明式 UI 范式,全部逻辑收敛在一个 @Entry @Component struct Index 主组件中,通过 @State 装饰器驱动视图刷新。架构上可分为以下几层:

  • 常量层PALETTE 配色对象、BOTTOM_TABS/TOP_TABS 导航配置、BRICKS(18 款套装)/SWAPS(8 条交换帖)/MOCS(5 份图纸)/FIGS(9 个人仔)/COLLECTIONS(6 个图鉴收藏)/SET_REVIEWS(3 条点评)六大业务数据数组、TRADE_LABELS/TRADE_BARS 全城周交易量柱状图数据、CONDITION_OPTIONS/PRICE_LEVELS/PLACES 弹框选项数组。
  • 接口层ColorPalette/TabItem/ParticleItem/BrickItem/SwapItem/MocItem/FigItem/CollectionItem/SetReview 共 9 个 interface(所有应用中 interface 最多的),强类型约束所有数据结构。
  • 工具函数层catColor(套装类型 6 色映射)、catLabel(套装类型 6 档文案)、rarityLabel(稀有度 4 档文案)、rarityColor(稀有度 4 色映射)、tradeBarHeight(周交易量柱状图高度换算)、rateColor(收藏进度 3 色映射)、swapBadge(交换帖点赞 3 档徽章),共 7 个工具函数。
  • 筛选函数层retiredSets/mocSets/figSets/techSets/streetSets/classicSets/cityLeft/cityRight/hotSets,共 9 个筛选函数,为 6 大顶部 Tab 和热销榜提供数据来源。
  • 组件层:主组件 Index 内部通过 @Builder 装饰器定义了头部、顶部 Tab、底部 Tab、6 个集市内容构建器、交换/图鉴/我的内容构建器、4 个弹窗构建器,共 19 个 @Builder 方法。
  • 状态层:19 个 @State 变量 + 1 个 private timerId,覆盖当前 Tab、弹窗显隐、表单数据(含 pickedPlaces 布尔数组实现多选)、粒子数组、定时器 ID。
  • 辅助方法层pickedWant(i)(交换弹窗多选判断,i%2===0)和 toggleWant(i)(切换交换想换选项),用于发起交换弹窗的多选逻辑。
  • 生命周期层aboutToAppear 中构建粒子并启动 setInterval 80ms 定时器;aboutToDisappearclearInterval 释放资源。

QQ乐高图纸Tab与人仔Tab运行效果

主组件的 build() 方法以 Stack 为根容器,底层是 Column 布局的主界面(头部 + 滚动内容区 + 底部 Tab),上层叠加 ForEach 渲染的粒子层(水平向右飘动),最上层是 4 个条件渲染的模态弹窗遮罩。这种 Stack 分层结构保证了粒子始终浮在内容之上、弹窗始终浮在最顶层。

三、色彩系统:乐高蓝主色 + 亮黄强调 + 乐高红危险 + 明快乐高风

应用的配色通过 ColorPalette 接口约束,由 PALETTE 常量统一提供,共 10 个色值:

色值名 HEX 用途
bg #F4F8FF 浅蓝页面底色,明快乐高风核心
card #FFFFFF 纯白卡片背景
blue #0057A7 乐高蓝主色,按钮、标签、激活文字、柱状图
yellow #FFD400 亮黄强调色,购物车角标、顶部下划线、时间轴、步骤序号
deep #123052 深蓝深色,主文字、横滑大卡渐变终点
red #E8483C 乐高红危险色,价格、下架按钮、绝版稀有度高亮、我想换栏
green #00A25B 乐高绿,人仔价格、面交好评、收藏进度高
sub #7A8FA6 灰蓝次级文字
line #E4EDF8 分割线与边框,浅蓝

除了这 10 个基础色,应用还在多个场景中引入了辅助色以丰富视觉层次:

  • 套装类型映射catColor 函数为 6 类套装分配专属色——绝版 #E8483C 乐高红、MOC 图纸 #7C4DFF 紫、人仔 #00A25B 乐高绿、科技 #E86A5B 橙红、街景 #1E88E5 亮蓝、经典 #0057A7 乐高蓝。这一函数在各 Tab 中广泛使用,用颜色直观区分套装类型。
  • 稀有度映射rarityLabel 函数为稀有度分配 4 档文案(>=95 传世级/>=80 稀有级/>=60 紧俏级/流通级),rarityColor 函数为稀有度分配 4 色(>=95 乐高红/>=80 橙/>=60 亮蓝/乐高绿)。这两个函数在绝版 Tab 横滑大卡、街景 Tab 紧凑列表、套装详情弹窗中使用,用颜色和文案直观表达套装稀有度。
  • 收藏进度映射rateColor 函数为收藏进度分配 3 色(>=55 乐高绿/>=40 亮黄/乐高红),在图鉴 Tab 收藏进度卡和进度条中使用。
  • 交换帖徽章映射swapBadge 函数为点赞数分配 3 档徽章(>=50 🔥热门/>=30 👍热度/🌱新帖),在交换帖列表中使用。
  • 危险/下架#E8483C 乐高红,用于下架按钮、确认下架、价格、绝版稀有度高亮、我想换栏。
  • 成功/好评#00A25B 乐高绿,用于人仔价格、面交好评、收藏进度高。

QQ乐高色彩系统与绝版Tab运行效果

渐变的运用是本应用色彩表达的重点:头部使用 linearGradient angle:135 colors:[[#0057A7,0],[#1E88E5,0.7],[#42A5F5,1]] 三色渐变(从乐高蓝到亮蓝到浅蓝,模拟乐高积木从深蓝到浅蓝的渐变层次);全城 Tab banner 使用亮黄渐变 #FFD400→#FFB300;绝版 Tab 横滑大卡使用蓝到深蓝渐变 #0057A7→#123052;科技 Tab 对决卡左卡蓝到深蓝渐变、右卡灰到深蓝渐变;发布出售弹窗头使用蓝渐变 #0057A7→#1E88E5;套装详情弹窗头使用亮黄渐变 #FFD400→#FFB300;我的 Tab 个人卡使用蓝渐变 #0057A7→#1E88E5。发起交换弹窗头使用纯亮黄 #FFD400 底(深字,是所有弹窗中唯一用亮黄浅色头的),与其他渐变头形成鲜明对比。不同弹窗通过头色快速区分交互类型。

四、数据模型设计:9 个 interface 强类型约束(所有应用中最多)

应用定义了 9 个 interface(所有应用中 interface 最多的),覆盖配色、导航、粒子、套装、交换帖、MOC 图纸、人仔、图鉴收藏、点评九大领域:

4.1 ColorPalette、TabItem 与 ParticleItem

interface ColorPalette {
  bg: string; card: string; blue: string; yellow: string;
  deep: string; red: string; green: string; sub: string; line: string;
}

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

interface ParticleItem {
  x: number;
  emoji: string;
  size: number;
  speed: number;
  opacity: number;
}

ParticleItem 与垂直飘动应用不同——没有 id 和 y 字段,只有 x(水平位置,单位 px)、emojisizespeedopacity。y 坐标在渲染时通过 80 + (i % 5) * 118 计算(5 行分布),x 用 px 而非百分比,这是因为粒子采用水平向右飘动,x 坐标需要精确到像素才能控制飘动速度和边界。

4.2 BrickItem 套装模型(核心,17 字段所有应用中最多)

interface BrickItem {
  id: number; name: string; eng: string; cat: string;
  price: number; parts: number; rarity: number; year: number;
  rating: number; starStr: string; seller: string; tag: string;
  desc: string; want: string; sales: number; emoji: string;
}

套装模型是应用最复杂的数据结构,17 个字段(所有应用中字段最多的)覆盖了乐高套装的完整信息:id 唯一标识、name 套装名(含编号如 10197 消防局)、eng 英文名、cat 类型(retired/moc/fig/tech/street/classic)、price 价格、parts 零件数、rarity 稀有度(0-100)、year 发行年份、rating 评分、starStr 星号字符串、seller 卖家、tag 标签(绝版/绝版神物/初代神作等)、desc 描述、want 卖家想换什么、sales 已售数量、emoji 图标。

engpartsrarityyearwant 是本应用数据模型的特色字段:eng 记录英文名增加专业感,parts 记录零件数(乐高圈核心参数),rarity 记录稀有度(0-100,配合 rarityLabel/rarityColor 表达稀缺程度),year 记录发行年份(绝版套装的核心价值指标),want 记录卖家想换什么(二手交易社区的以物换物特色字段)。

BRICKS 常量预置了 18 款套装,从 10197 消防局/6289 红魔海盗船/375 骑士城堡(3 款绝版),到 MOC-88213 樱花城堡/MOC-11045 蒸汽飞艇/MOC-20331 四合院(3 款 MOC 图纸),到星战白兵 1999 初版/哈利波特斯内普/蝙蝠侠 2012 版(3 款人仔),到 42115 兰博基尼/42083 布加迪奇龙/42146 利勃海尔(3 款科技),到 10218 宠物商店/10255 集会广场/10278 警察局(3 款街景),到 10305 狮王城堡/21318 老友记公寓/21335 电动灯塔(3 款经典),覆盖 6 大类型,每款套装的 eng/price/parts/rarity/year/seller/tag/desc/want/sales/emoji 都精心设计以匹配乐高特色。

4.3 SwapItem 交换帖模型、MocItem 图纸模型、FigItem 人仔模型、CollectionItem 图鉴模型与 SetReview 点评模型

interface SwapItem {
  id: number; user: string; avatar: string; offer: string;
  want: string; time: string; likes: number; content: string; place: string;
}

interface MocItem {
  id: number; name: string; type: string; pages: number;
  author: string; likes: number; downloads: number; emoji: string;
  steps: string[];
}

interface FigItem {
  id: number; name: string; series: string; price: number;
  condition: string; seller: string; emoji: string; desc: string;
}

interface CollectionItem {
  id: number; name: string; series: string; owned: number;
  total: number; rate: number; emoji: string;
}

interface SetReview {
  id: number; user: string; setName: string; starStr: string; content: string;
}

SwapItem 10 字段记录交换帖,SWAPS 预置 8 条(乐高搬运工/AFOL老张/小人仔批发商/国风砖筑/科技控小马/街景集邮家/初代情怀党/图纸搬运工),offer 记录我提供什么、want 记录我想换什么、place 记录面交地点,是二手交换社区的核心数据。MocItem 10 字段记录 MOC 图纸,MOCS 预置 5 份(樱花城堡/蒸汽朋克飞艇/老北京四合院/复古小酒馆/泰坦巨轮),pages 记录页数、downloads 记录下载量、steps 是字符串数组记录搭建步骤(带亮黄圆形序号渲染),是图纸 Tab 的核心数据。FigItem 8 字段记录人仔,FIGS 预置 9 个(星战白兵/斯内普/蝙蝠侠/克隆人军官/哈利波特/达斯维达/印第安纳琼斯/孙悟空/马里奥),series 记录系列、condition 记录成色,是人仔 Tab 的核心数据。CollectionItem 7 字段记录图鉴收藏,COLLECTIONS 预置 6 个(街景系列/科技旗舰/城堡系列/星战载具/Ideas 系列/哈利波特),owned 记录已收数量、total 记录总数、rate 记录收藏进度百分比,是图鉴 Tab 的核心数据。SetReview 5 字段记录点评,starStr 用 Emoji 星号字符串直接渲染,SET_REVIEWS 预置 3 条(评价用户名「AFOL 评测局」「砖块医生」「科技件玩家」充满乐高圈梗文化)。

五、工具函数与筛选函数:7 工具 + 9 筛选驱动视觉映射与数据分发

应用定义了 7 个工具函数和 9 个筛选函数,工具函数负责将数据值映射为视觉属性,筛选函数负责为 6 大顶部 Tab 和热销榜提供数据来源。

5.1 七个工具函数

function catColor(cat: string): string {
  if (cat === 'retired') return '#E8483C'
  if (cat === 'moc') return '#7C4DFF'
  if (cat === 'fig') return '#00A25B'
  if (cat === 'tech') return '#E86A5B'
  if (cat === 'street') return '#1E88E5'
  return '#0057A7'
}

function catLabel(cat: string): string {
  if (cat === 'retired') return '绝版回血'
  if (cat === 'moc') return '图纸分享'
  if (cat === 'fig') return '人仔交易'
  if (cat === 'tech') return '科技旗舰'
  if (cat === 'street') return '街景收藏'
  return '经典好价'
}

function rarityLabel(r: number): string {
  if (r >= 95) return '传世级'
  if (r >= 80) return '稀有级'
  if (r >= 60) return '紧俏级'
  return '流通级'
}

function rarityColor(r: number): string {
  if (r >= 95) return '#E8483C'
  if (r >= 80) return '#FF8A00'
  if (r >= 60) return '#1E88E5'
  return '#00A25B'
}

function tradeBarHeight(v: number): string {
  return Math.round(v / 156 * 96).toString() + 'vp'
}

function rateColor(r: number): string {
  if (r >= 55) return '#00A25B'
  if (r >= 40) return '#FFD400'
  return '#E8483C'
}

function swapBadge(likes: number): string {
  if (likes >= 50) return '🔥热门'
  if (likes >= 30) return '👍热度'
  return '🌱新帖'
}
  • catColor:将套装类型映射为 6 色(绝版乐高红/MOC 紫/人仔乐高绿/科技橙红/街景亮蓝/经典乐高蓝),是本应用最具特色的工具函数,用颜色直观区分套装类型。
  • catLabel:将套装类型映射为 6 档文案(绝版回血/图纸分享/人仔交易/科技旗舰/街景收藏/经典好价)。
  • rarityLabel:将稀有度映射为 4 档文案(传世级/稀有级/紧俏级/流通级),「传世级」形容 95 分以上的绝版神物(如 375 骑士城堡 100 分),是乐高圈梗。
  • rarityColor:将稀有度映射为 4 色(>=95 乐高红/>=80 橙/>=60 亮蓝/乐高绿),用颜色直观表达稀有度。
  • tradeBarHeight:将周交易量换算为柱状图高度 vp(v / 156 * 96,以周六最高 156 为基准归一化到 96vp)。
  • rateColor:将收藏进度映射为 3 色(>=55 乐高绿/>=40 亮黄/乐高红),在图鉴 Tab 收藏进度卡和进度条中使用。
  • swapBadge:将交换帖点赞数映射为 3 档徽章(🔥热门/👍热度/🌱新帖),在交换帖列表中使用。

5.2 九个筛选函数

function retiredSets(): BrickItem[] { return [BRICKS[0], BRICKS[1], BRICKS[2]] }
function mocSets(): BrickItem[] { return [BRICKS[3], BRICKS[4], BRICKS[5]] }
function figSets(): BrickItem[] { return [BRICKS[6], BRICKS[7], BRICKS[8]] }
function techSets(): BrickItem[] { return [BRICKS[9], BRICKS[10], BRICKS[11]] }
function streetSets(): BrickItem[] { return [BRICKS[12], BRICKS[13], BRICKS[14]] }
function classicSets(): BrickItem[] { return [BRICKS[15], BRICKS[16], BRICKS[17]] }
function cityLeft(): BrickItem[] { return [BRICKS[16], BRICKS[17]] }
function cityRight(): BrickItem[] { return [BRICKS[15]] }
function hotSets(): BrickItem[] { return [BRICKS[3], BRICKS[5], BRICKS[16], BRICKS[17], BRICKS[9]] }

9 个筛选函数中,retiredSets/mocSets/figSets/techSets/streetSets/classicSets 对应绝版/图纸/人仔/科技/街景/经典六个顶部 Tab,cityLeftcityRight 是全城 Tab 双列瀑布流的左右列数据(左列 2 款、右列 1 款,形成高度错落的瀑布流效果),hotSets 是热销 TOP5 榜单数据。与其他应用使用 filter() 不同,本应用的筛选函数直接通过数组索引取数据(BRICKS[0]/BRICKS[1] 等),因为 BRICKS 数组已经按类型顺序排列(0-2 绝版、3-5 MOC、6-8 人仔、9-11 科技、12-14 街景、15-17 经典),直接索引比 filter 更高效。这种「7 工具函数 + 9 筛选函数」的分层设计,让数据映射与数据分发各司其职,模板中只需调用对应函数即可获得正确数据,保持模板简洁。

六、粒子动画系统:14 粒子最快速度水平向右飘动(积木传送带意象)

粒子系统是本应用的动态灵魂,选用乐高砖 🧱、拼图 🧩、闪光 ✨、黄块 🟨、蓝块 🟦 五种图标循环,以所有应用中最快的速度水平向右飘动,模拟积木在传送带上向右移动、乐高工厂出件口一块接一块送出的场景。

6.1 粒子构建:14 粒子 5 图标循环(无 id/y 字段)

buildParticles(): void {
  const emojis: string[] = ['🧱', '🧩', '✨', '🟨', '🟦']
  const list: ParticleItem[] = []
  for (let i = 0; i < 14; i++) {
    list.push({
      x: Math.random() * 340,
      emoji: emojis[i % emojis.length],
      size: 12 + Math.random() * 10,
      speed: 0.6 + Math.random() * 1.4,
      opacity: 0.2 + Math.random() * 0.4
    })
  }
  this.particles = list
}

14 个粒子通过 Math.random() 生成随机分布的 x 坐标(0~340px),图标在 🧱🧩✨🟨🟦 五种中循环(乐高砖/拼图/闪光/黄块/蓝块,黄块蓝块直接对应乐高经典配色),size 范围 12~22,opacity 范围 0.20~0.60,speed 范围 0.6~2.0(每个粒子独立速度,是所有应用中最快的粒子速度,模拟乐高工厂出件的快节奏)。与垂直飘动应用不同,ParticleItem 没有 idy 字段——y 坐标在渲染时通过 80 + (i % 5) * 118 计算(5 行分布,每行间隔 118px),x 用 px 而非百分比,这是因为粒子采用水平向右飘动,x 坐标需要精确到像素才能控制飘动速度和边界。

6.2 粒子驱动:水平向右飘动 + 左侧循环

driftParticles(): void {
  for (let i = 0; i < this.particles.length; i++) {
    this.particles[i].x += this.particles[i].speed
    if (this.particles[i].x > 360) {
      this.particles[i].x = -20
    }
  }
  this.particles = this.particles.slice()
}

关键点:每帧将每个粒子的 x 坐标加上自身 speed(水平向右飘动),当 x > 360(飘出右侧)时重置为 -20(从左侧重新进入),实现无限循环。y 坐标保持不变(5 行固定分布)。这一方向设计模拟了积木在传送带上向右移动、乐高工厂出件口一块接一块送出的场景,与垂直飘动的常规粒子形成鲜明反差,是所有应用中唯二采用水平飘动粒子的应用(与奶茶应用并列,但图标 🧱🧩✨🟨🟦 更贴合乐高主题,黄块蓝块直接对应乐高经典配色)。配合最快的速度(0.6~2.0)和最快的刷新频率(80ms),营造出乐高工厂出件的快节奏与乐高爱好者的拼搭快乐。

this.particles = this.particles.slice() 是关键——通过 slice() 创建新数组引用,触发 ArkTS 的状态更新和视图重渲染。如果直接修改数组元素而不改变引用,@State 不会触发更新。

6.3 生命周期与渲染

aboutToAppear(): void {
  this.buildParticles()
  this.timerId = setInterval(() => {
    this.driftParticles()
  }, 80)
}

aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId)
    this.timerId = -1
  }
}

aboutToAppear 中先构建粒子数组,再启动 setInterval 80ms 定时器(所有应用中最快的刷新频率,约 12.5fps,配合最快的粒子速度,模拟乐高工厂出件的快节奏),将定时器 ID 存入 private timerIdaboutToDisappear 中清除定时器并重置为 -1。

渲染层在 Stack 中通过 ForEach 遍历粒子,position 使用 { x: p.x, y: 80 + (i % 5) * 118 }(x 用 px,y 用 5 行分布计算),ForEach 的 key 函数使用 'pt' + i.toString()(用索引 i 而非粒子 id,因为 ParticleItem 没有 id 字段)。

七、双层导航架构:底部 4 Tab + 顶部 6 Tab

应用采用双层 Tab 导航,底部 Tab 切换大模块、顶部 Tab 在「集市」模块内切换套装类型分类。

7.1 头部 headerBuilder:蓝三色渐变 + 搜索条 + 购物车角标

头部使用 linearGradient angle:135 colors:[[#0057A7,0],[#1E88E5,0.7],[#42A5F5,1]] 三色渐变(从乐高蓝到亮蓝到浅蓝,模拟乐高积木从深蓝到浅蓝的渐变层次),底部圆角 18。头部包含:

  • 左侧:🧱 积木回血社 22sp 粗体白字 + 「出闲置·收绝版·换心头好」11sp 浅蓝字。
  • 右侧:🔎 搜索 20sp + 📦 包裹 20sp + 🛍️ 购物车 20sp 带「3」角标(亮黄 #FFD400 底深蓝字 9sp,margin top -6 形成角标效果)。
  • 搜索条:🔍 搜索:绝版 / 人仔 / MOC图纸 12sp 灰蓝字 + 「🔨 我的求购」按钮(亮黄 #FFD400 底深蓝字圆角 10)。

头部是所有应用中最复杂的头部之一——三色渐变 + 三图标 + 购物车角标 + 搜索条 + 我的求购按钮,完整模拟了电商/二手交易 App 的头部交互体验。

7.2 顶部 Tab topTabsBuilder(仅集市 Tab 显示,6 Tab 等宽 + 双下划线)

6 个 Tab 等宽排列(全城🏙️/绝版🏆/图纸📐/人仔🪖/科技⚙️/街景🏘️),每个 Tab layoutWeight(1) 等宽。每个 Tab 显示图标 17sp + 文字 12sp。激活态为 #0057A7 乐高蓝字粗体 + 亮黄 #FFD400 圆角下划线(Divider width16 strokeWidth3 borderRadius2),非激活态为 #7A8FA6 灰蓝字 + #F4F8FF 浅蓝底圆角下划线(非激活也有下划线,是本应用顶部 Tab 的特色细节,形成双下划线对比效果)。顶部 Tab 使用纯白底圆角 14 margin top 10。点击切换 currentTopTab

7.3 底部 Tab bottomTabsBuilder(纯白底 + 顶部圆角)

4 个 Tab(集市🏪/交换🤝/图鉴📖/我的🧱)等宽排列,激活态为 #0057A7 乐高蓝字粗体,非激活态为 #7A8FA6 灰蓝字。底部 Tab 使用纯白底,顶部圆角 18(borderRadius: { topLeft: 18, topRight: 18 }),与头部的底部圆角呼应,形成圆润的乐高积木视觉隐喻。点击切换 currentBottomTab

八、集市 Tab:6 大类型差异化布局

集市 Tab 是应用最核心的模块,6 个顶部 Tab 对应 6 种完全不同的布局风格,从全城的双列瀑布流、绝版的横滑大卡+绝版稀有度柱状图+捡漏榜、图纸的 MOC 搭建时间轴、人仔的三列格子+交换行情、科技的旗舰超跑对决卡+侧栏统计、到街景的收藏标签云+紧凑列表+集齐彩蛋,每种套装类型都有专属的视觉语言。

8.1 全城 cityContent:积木回血节 banner + 双列瀑布流

全城 Tab 由三部分组成:

  1. 积木回血节 bannerangle:120 亮黄渐变 #FFD400→#FFB300,「🧱 积木回血节 · 第2季」18sp 粗体深蓝字 + 「绝版专场 3天限时 · 卖家免服务费」12sp 灰蓝字 + 两个标签按钮(🏆 绝版捡漏 / 📐 MOC图纸,纯白底深蓝字圆角 12)。
  2. 全城新上架标题:「✨ 全城新上架」15sp 粗体深蓝字。
  3. 双列瀑布流Row 左右两列,左列 cityLeft() 2 款(21318 老友记公寓/21335 电动灯塔)纯白底卡,右列 cityRight() 1 款(10305 狮王城堡)#FFF8DC 浅黄底卡。左右列卡片高度错落(左列 emoji margin top 16、右列 margin top 24;左列 desc maxLines3、右列 maxLines4;左列底部 margin 14、右列 20),形成真正的瀑布流效果。每张卡片包含:emoji 34sp + 套装名 14sp 粗体 + 英文名 9sp 灰字 + desc 11sp 灰字(maxLines3/4)+ 价格 15sp 粗体乐高红 + 标签(tag 浅蓝底乐高蓝字 / starStr 星号)。点击打开套装详情弹窗。

双列瀑布流是全城 Tab 的核心布局,通过左右列卡片高度错落模拟电商 App 的商品瀑布流,适合全城新上架的「上新展示」。

8.2 绝版 retiredContent:横滑大卡 + 绝版稀有度柱状图 + 捡漏榜

绝版 Tab 由四部分组成:

  1. 标题:「🏆 绝版回血专区」15sp 粗体深蓝字。
  2. 横滑大卡Scroll horizontal 横向滚动,retiredSets() 筛选出 3 款绝版(10197 消防局/6289 红魔海盗船/375 骑士城堡),每个渲染为 width:160 的蓝到深蓝渐变 #0057A7→#123052 底圆角 16 卡片,包含:emoji 40sp + 套装名 15sp 粗体白字 + 英文名·年份 9sp 浅蓝字 + 价格 16sp 粗体亮黄字 + rarityLabel 亮黄底深蓝字标签。横滑大卡适合绝版套装的「神物展示」,蓝到深蓝渐变底白字营造绝版的深邃与珍贵感。
  3. 绝版稀有度柱状图:标题「📈 绝版稀有度排行」,下方是纯白底圆角 14 卡片,内含 Row 布局的 3 根柱子retiredSets() 3 款绝版,height 直接用 t.rarity.toString() + 'vp',即稀有度分数直接作为柱子高度 vp),使用高亮双色——i === 2(375 骑士城堡,rarity 100)乐高红 #E8483C 高亮,其他两款亮蓝 #1E88E5,柱子顶部标注稀有度数字(乐高蓝字),底部标注年份。3 根柱子等宽排列,375 骑士城堡 100 分最高形成「传世级登顶」可视化,底部文字「375 骑士城堡以100分稀有度登顶,市面流通不足百套」。绝版稀有度柱状图是本应用最具特色的数据可视化——直接用稀有度分数作为柱子高度,3 款绝版对比,骑士城堡 100 分红高亮,直观表达绝版套装的稀缺程度。
  4. 捡漏榜:标题「🕵️ 今日捡漏榜」,retiredSets() 3 款绝版,每行左侧序号(第 1 名乐高红粗体,其他灰字)+ emoji + 套装名 + rarityLabel·want,右侧价格。点击打开套装详情弹窗。

绝版 Tab 通过横滑大卡 + 绝版稀有度柱状图 + 捡漏榜三段式布局,完整展示绝版套装的神物地位和稀缺数据,是数据可视化最有特色的 Tab。

8.3 图纸 mocContent:MOC 搭建时间轴

图纸 Tab 由标题 + MOC 搭建时间轴组成:

  1. 标题:「📐 MOC图纸 · 搭建说明书」15sp 粗体深蓝字。
  2. MOC 搭建时间轴MOCS 5 份图纸(樱花城堡/蒸汽朋克飞艇/老北京四合院/复古小酒馆/泰坦巨轮),每行左侧是时间轴节点(emoji 20sp + 亮黄 #FFD400 竖线 layoutWeight(1) 连接,前 4 份有竖线,最后一份没有),右侧是内容卡(第 2 份蒸汽朋克飞艇 #FFF8DC 浅黄底高亮,其他纯白底),包含:
    • 头部行:图纸名 + ⬇下载量(乐高蓝字)
    • by 作者·类型·页数 10sp 灰字
    • 步骤列表:ForEach(m.steps) 渲染步骤数组,每步左侧亮黄圆形序号(#FFD400 亮黄底深蓝字 18x18 圆角 9)+ 步骤文字 10sp 深灰字,整体 #F4F8FF 浅蓝底圆角 10 padding 10
    • 底部行:❤️点赞数(乐高红)+ 📐图纸可下载(乐高蓝字,右对齐)

MOC 搭建时间轴是图纸 Tab 的核心布局,通过左侧 emoji + 亮黄竖线连接,模拟 MOC 图纸从第 1 步到第 N 步的搭建历程,亮黄竖线和圆形序号直接呼应乐高亮黄主题色,步骤列表带序号让茶友可以按步骤搭建,适合 MOC 图纸的「搭建说明」主题。

QQ乐高图纸Tab与人仔Tab运行效果

8.4 人仔 figContent:三列格子 + 人仔交换行情

人仔 Tab 由三部分组成:

  1. 标题:「🪖 人仔交易 · 摆件战场」15sp 粗体深蓝字。
  2. 三列格子Flex wrap justifyContent:SpaceBetween 三列布局,FIGS 9 个人仔(星战白兵/斯内普/蝙蝠侠/克隆人军官/哈利波特/达斯维达/印第安纳琼斯/孙悟空/马里奥),每个格子 width:31%,纯白底圆角 12,包含:emoji 26sp + 人仔名 11sp 粗体(maxLines1)+ series 8sp 灰字 + 价格 13sp 粗体乐高红。三列格子适合人仔的「快取展示」,模拟人仔收藏柜的紧凑布局。
  3. 人仔交换行情:标题「🤝 人仔交换行情」,遍历 FIGS 筛选 id % 2 === 0 的 4 个人仔(斯内普/克隆人军官/达斯维达/马里奥),每行 emoji + 人仔名·condition 成色 + desc + 价格(乐高绿字)。人仔交换行情是人仔 Tab 的特色内容,展示人仔的成色和交换价格,增加内容丰富度。

8.5 科技 techContent:旗舰超跑对决卡 + 列表 + 侧栏统计

科技 Tab 由三部分组成:

  1. 标题:「⚙️ 科技旗舰 · 机械对决」15sp 粗体深蓝字。
  2. 旗舰超跑对决卡Row 三列布局,左列 42115 兰博基尼(蓝到深蓝渐变 #0057A7→#123052 底白字,emoji + 套装名 + 零件数·现价),中间 VS(18sp 粗体乐高蓝字 + 「本周」9sp 灰字),右列 42083 布加迪(灰到深蓝渐变 #7A8FA6→#123052 底白字,emoji + 套装名 + 零件数·现价)。旗舰超跑对决卡是科技 Tab 的核心特色,通过左右对决 + 中间 VS 模拟科技旗舰超跑的人气对决,蓝渐变 vs 灰渐变形成强烈视觉对比。
  3. 列表 + 侧栏统计Row 两列布局,左列 techSets() 筛选出 3 款科技套装列表(emoji + 套装名 + tag·零件数 + 价格),右列侧栏统计(12 本周科技件成交浅蓝底 / 3.2万 科技件总交易额浅黄底 / 86% 物流好评率纯白底,三列纵向排列)。侧栏统计是科技 Tab 的特色数据展示,物流好评率 86% 反映了二手科技套装的交易信任度。

QQ乐高科技Tab与街景Tab运行效果

8.6 街景 streetContent:收藏标签云 + 紧凑列表 + 集齐彩蛋

街景 Tab 由四部分组成:

  1. 标题:「🏘️ 街景收藏 · 城市拼图」15sp 粗体深蓝字。
  2. 收藏标签云Flex wrap 渲染 COLLECTIONS 6 个图鉴收藏(街景系列/科技旗舰/城堡系列/星战载具/Ideas 系列/哈利波特),每个标签 emoji + name + owned/total(浅蓝底深蓝字)。收藏标签云是街景 Tab 的特色内容,展示各系列的收藏进度。
  3. 紧凑列表streetSets() 筛选出 3 款街景套装(10218 宠物商店/10255 集会广场/10278 警察局),每行 emoji + 套装名 + rarityLabel 标签(rarityColor 映射色,如紧俏级亮蓝底/传世级红底)+ desc(maxLines1)+ 价格。第 2 款 10255 集会广场 #E4F0FB 浅蓝底高亮。
  4. 集齐彩蛋提示:「💬 集齐街景三杰可召唤隐藏彩蛋:宠物商店+集会广场+警察局」10sp 灰字。集齐彩蛋是街景 Tab 的特色互动,增加街景收藏的趣味性和目标感。

九、交换 Tab:统计 banner + 交换帖列表(我提供/我想换双栏)

交换 Tab swapContent 由统计 banner + 交换帖列表组成:

  1. 统计 banner:纯白底圆角 14,等宽四列——「156 在交换中」乐高蓝字、「42 今日成交」橙字、「98% 面交好评」乐高绿字、「🤝 发交换」按钮(橙底白字圆角 14,点击打开发起交换弹窗)。
  2. 交换帖列表:8 条交换帖,每个卡片包含:
    • 头部行:avatar 26sp + 用户名 13sp 粗体 + swapBadge 徽章(🔥热门/👍热度/🌱新帖,橙字浅黄底)+ 时间·面交地点 9sp 灰字 + ⋯ 更多按钮(点击打开下架弹窗)。
    • 交换内容双栏Row 三列布局,左列「我提供」(浅蓝 #E4F0FB 底,9sp 乐高蓝字标题 + offer 11sp 粗体深蓝字),中间 亮黄箭头 14sp,右列「我想换」(浅红 #FFEBEE 底,9sp 乐高红字标题 + want 11sp 粗体深蓝字)。我提供/我想换双栏是交换 Tab 的核心特色,用浅蓝/浅红双色区分提供和需求,⇄ 亮黄箭头直观表达交换意向。
    • 内容:content 11sp 深灰字 lineHeight:16。
    • 底部行:📍 面交地点(乐高蓝字浅蓝底)+ ❤️点赞数(乐高红字,右对齐)。

交换 Tab 通过统计 banner + 交换帖列表(我提供/我想换双栏)的布局,完整展示乐高以物换物社区的内容,通过双栏交换内容和 ⇄ 箭头直观表达交换意向,是乐高二手交易社区的交换核心区。

QQ乐高交换Tab运行效果

十、图鉴 Tab:收藏进度卡 + 进度条 + 成就墙

图鉴 Tab collectionContent 由标题栏 + 收藏进度卡列表 + 图鉴成就墙组成:

  1. 标题栏:左侧「📖 我的乐高图鉴」16sp 粗体 + 「记录每一套心头好的收藏进度」10sp 灰字,右侧「✚ 登记」按钮(乐高蓝底白字圆角 14,点击打开发布出售弹窗复用)。
  2. 收藏进度卡列表:6 个图鉴收藏(街景系列/科技旗舰/城堡系列/星战载具/Ideas 系列/哈利波特),每个卡片包含:
    • 头部行:emoji 28sp + 收藏名 14sp 粗体 + series·已收 owned/total 10sp 灰字 + rate% 15sp 粗体(rateColor 映射色,>=55 绿/>=40 黄/红)。
    • 进度条Row 两列,左列 width:rate% 高度 6 圆角 3(rateColor 填充色),右列 width:(100-rate)% 高度 6 圆角 3(浅蓝 #E4EDF8 底)。进度条是图鉴 Tab 的核心特色,用颜色填充直观表达收藏进度。
    • 第 5 个 Ideas 系列 #FFF8DC 浅黄底高亮。
  3. 图鉴成就墙:纯白底圆角 14,标题「🏅 图鉴成就墙」,Flex wrap 渲染 4 个成就(🎖️街景三杰浅蓝底乐高蓝字/🏆绝版猎人浅红底乐高红字/🪖人仔百夫长浅黄底橙字/📐MOC搬运工浅绿底乐高绿字)。

图鉴 Tab 通过收藏进度卡 + 进度条 + 成就墙的布局,完整展示乐高爱好者的收藏进度,通过 rateColor 三色进度条直观表达收藏完成度,是乐高收藏社区的图鉴核心区。

十一、我的 Tab:个人卡 + 求购清单 + 常去面交点

我的 Tab mineContent 由三部分组成:

  1. 个人卡angle:120 蓝渐变 #0057A7→#1E88E5(从乐高蓝到亮蓝),圆角 16,包含:
    • 头部行:🧱 40sp + 「积木收藏家阿乐」16sp 粗体白字 + 「LV.9 骨灰级砖友 · 信用分 986」10sp 浅蓝字 + >箭头。
    • 三列统计:「38 在售套装」「12 求购中」「¥8.6万 累计成交额」,均亮黄字。累计成交额 ¥8.6万 是我的 Tab 的特色数据,让砖友直观看到自己在乐高二手交易中的投入与回报。
  2. 我的求购清单:纯白底圆角 14,标题「🔨 我的求购清单」,遍历 BRICKS 筛选 id === 13 || id === 16 || id === 18 的 3 款(10218 宠物商店/10305 狮王城堡/21335 电动灯塔),每行 emoji + 套装名 + 预算 price 内·want + 「出价中」橙标签浅黄底。
  3. 常去面交点:纯白底圆角 14,标题「📍 常去面交点」,PLACES 5 个面交地点(光谷广场站/江汉路站/洪山广场站/楚河汉街站/线上互换),每行 emoji(🚇/🏬/🚌/🏪/📦)+ 地点名 + 地点描述(光谷站台/江汉路天桥/洪山转盘/楚河入口/顺丰网点)。

我的 Tab 通过个人卡 + 求购清单 + 常去面交点三段式布局,完整展示砖友的个人数据和交易偏好,累计成交额 ¥8.6万 和信用分 986 是我的 Tab 的特色数据,常去面交点增加了二手交易的线下便利性。

QQ乐高图鉴Tab与我的Tab运行效果

十二、4 个模态弹窗:新增/新增/删除/详情四态闭环

应用实现了 4 个模态弹窗,覆盖发布出售(新增)、发起交换(新增)、下架(删除)、套装详情(查询)四种交互形态,每个弹窗都有专属的头色与表单结构。

12.1 发布出售 sellModal:蓝渐变头 + 选套装/成色/价格步进器/描述

出售弹窗 width:88% height:76% 圆角 18,顶部是 angle:120 蓝渐变头 #0057A7→#1E88E5(从乐高蓝到亮蓝),标题「📦 发布出售」17sp 粗体白字 + 「闲置回血,绝版不愁卖」11sp 浅蓝字。

滚动内容区包含:

  • 选套装Flex wrap 8 款套装(BRICKS 前 8 款,emoji + 套装名),选中态乐高蓝底白字,非选中浅蓝底深蓝字,点击切换 sellBrickIdx
  • 成色说明Flex wrap 5 个成色(CONDITION_OPTIONS:全新未拆/仅拆盒/9成新/8成新/有缺件),选中态 #FF8A00 橙底白字,非选中浅黄底橙字,点击切换 sellCondIdx
  • 价格步进器:「出售价格」标签 + 步进器(减浅蓝底乐高蓝字 / 中间 ¥N 纯白底深蓝字 / 加乐高蓝底白字,10-9999 步长 10,>2000 提示「💰 高价好物建议包邮」乐高红字,其他「实惠价更容易出手」灰字)。价格步进器是发布出售弹窗的特色字段,步长 10 适合乐高套装的价格区间,>2000 时给出包邮建议。
  • 宝贝描述:TextInput(placeholder「成色、缺件、配件情况如实描述…」),浅蓝底圆角 10。
  • 操作按钮:「取消」浅蓝底灰字 + 「上架出售」乐高蓝底白字粗体,均点击关闭弹窗。

12.2 发起交换 swapModal:亮黄头(唯一浅色头)+ 我提供/我想换多选/面交地点

交换弹窗 width:88% height:72% 圆角 18,顶部是纯亮黄 #FFD400(深字,是所有弹窗中唯一用亮黄浅色头的),标题「🤝 发起交换」17sp 粗体深蓝字 + 「用闲置换心头好,砖友互助」11sp 灰字。

滚动内容区包含:

  • 我提供Flex wrap 5 款套装(BRICKS 筛选 i%3===0 && i<15),选中态 #123052 深蓝底白字,非选中浅蓝底深蓝字,点击切换 swapOfferIdx
  • 我想换(可多选)Flex wrap 5 款套装(BRICKS 筛选 i%3=2 && i<15),选中态 #E8483C 乐高红底白字带「✓」前缀,非选中浅红底乐高红字,点击调用 toggleWant(i) 设置 swapWantIdx=i,通过 pickedWant(i) 辅助方法(i%2=0)判断选中状态。我想换多选是发起交换弹窗的特色功能,砖友可以选择多个想换的套装增加交换成功率。
  • 面交地点Flex wrap 5 个地点(PLACES),选中态乐高蓝底白字,非选中浅蓝底乐高蓝字,点击切换 swapPlaceIdx
  • 提示框:💡 「交换双方可自行协商补差价,面交请选择人流密集的公共场所,注意安全。」浅黄底圆角 10。面交安全提示是发起交换弹窗的特色内容,提醒砖友线下交易安全。
  • 操作按钮:「再想想」浅蓝底灰字 + 「发布交换帖」亮黄底深蓝字粗体,均点击关闭弹窗。

12.3 下架 deleteModal:🗑️ 警告小卡

下架弹窗是 4 个弹窗中最小的一个,width:72% 纯白底圆角 18,居中显示:

  • 🗑️ 垃圾桶图标 36sp
  • 「下架这条宝贝?」16sp 粗体深蓝字
  • 「下架后买家将无法搜索到这条信息,收藏和留言会一并清空,可重新上架。」11sp 灰字(lineHeight:17,居中)
  • 操作按钮:「留着」浅蓝底乐高蓝字 + 「确认下架」#E8483C 乐高红底白字粗体,均点击关闭弹窗。「留着」是下架弹窗的特色按钮文案,用乐高圈轻松的语气替代严肃的「取消」,符合明快乐高风。

下架弹窗通过 🗑️ 图标 + 乐高红确认按钮 + 详细后果说明(买家无法搜索/收藏留言一并清空/可重新上架),提醒用户操作可逆(可重新上架,与其他应用的不可恢复删除不同),符合二手交易平台的下架逻辑。

12.4 套装详情 detailModal:亮黄渐变头 + 四列统计 + 描述 + 想换 + 点评 + 操作

详情弹窗 width:88% height:78% 圆角 18,是 4 个弹窗中最高的一个,内容最丰富。顶部是 angle:135 亮黄渐变头 #FFD400→#FFB300(从亮黄到深黄):

  • emoji 40sp(通过 detailEmoji() 查找)
  • 套装名 18sp 粗体深蓝字(通过 detailName() 查找)
  • 英文名 10sp 灰字(通过 detailEng() 查找)
  • 标签行:tag 标签(乐高蓝底白字)+ rarityLabel 标签(亮黄底深蓝字)

滚动内容区包含:

  • 四列统计卡#F4F8FF 浅蓝底圆角 12,等宽四列——「售价 price」#E8483C 乐高红字、「零件数 parts」乐高蓝字、「稀有度 rarity」rarityColor 映射色、「发行年份 year」#FF8A00 橙字。
  • 宝贝描述:标题「🧾 宝贝描述」+ desc 11sp 深灰字 lineHeight:18 + 👤seller(乐高蓝字)+ starStr rating(橙字,右对齐)。
  • 想换:🤝 「卖家想换:want」11sp 乐高红字,浅红 #FFEBEE 底圆角 10。想换栏是套装详情弹窗的特色内容,展示卖家的以物换物意向,增加二手交易的交换可能性。
  • 圈友点评:标题「💬 圈友点评」+ ForEach(SET_REVIEWS) 渲染 3 条点评,每条为 #F4F8FF 浅蓝底圆角 10 卡片,包含 👤 + 用户名 + starStr(右对齐)+ content 10sp 灰字。评价用户名「AFOL 评测局」「砖块医生」「科技件玩家」充满乐高圈梗文化。
  • 操作按钮:「💬 留言问问」浅蓝底乐高蓝字 + 「🛒 立即购买」乐高红底白字粗体,均点击关闭弹窗。

QQ乐高弹窗运行效果

12.5 detail 系列方法与辅助方法:13 个查找方法 + 2 个辅助方法

详情弹窗通过 selectedBrickIdBRICKS 数组中查找对应套装,定义了 13 个方法(所有应用中查找方法最多的):detailName()detailEng()detailEmoji()detailPrice()detailParts()detailRarity()detailYear()detailSeller()detailTag()detailStar()detailRating()detailDesc()detailWant()。每个方法都通过 for 循环遍历 BRICKS 查找匹配 id 的套装,再返回对应字段,若未找到则返回默认值(空字符串/0/‘🧱’)。13 个查找方法对应 BrickItem 的 13 个展示字段,是数据驱动详情弹窗的完整实现。

此外还有 2 个辅助方法用于发起交换弹窗的多选逻辑:pickedWant(i) 返回 i % 2 === 0(判断第 i 个想换选项是否选中),toggleWant(i) 设置 this.swapWantIdx = i(切换想换选项)。这两个辅助方法实现了发起交换弹窗的我想换多选功能。

十三、特性对比表

特性维度 QQ乐高·积木回血社实现
主题风格 乐高蓝 #0057A7 + 亮黄 #FFD400 + 乐高红 #E8483C 明快乐高风
导航架构 底部 4 Tab(集市/交换/图鉴/我的)+ 顶部 6 Tab(全城/绝版/图纸/人仔/科技/街景),顶部 Tab 仅集市模块显示,6 Tab 等宽 layoutWeight(1) + 双下划线(激活亮黄/非激活浅蓝)
数据模型 9 个 interface(所有应用中最多,ColorPalette/TabItem/ParticleItem/BrickItem/SwapItem/MocItem/FigItem/CollectionItem/SetReview),BrickItem 17 字段(所有应用中最多,含 eng/parts/rarity稀有度/year发行年份/want想换),MocItem 含 steps 步骤数组,CollectionItem 含 owned/total/rate 收藏进度,ParticleItem 无 id/y 字段(水平飘动专用)
业务数据量 18 套装 + 8 交换帖 + 5 MOC 图纸 + 9 人仔 + 6 图鉴收藏 + 3 点评 + 7 周交易量柱状图
工具函数 7 个(catColor/catLabel/rarityLabel/rarityColor/tradeBarHeight/rateColor/swapBadge)
筛选函数 9 个(retiredSets/mocSets/figSets/techSets/streetSets/classicSets/cityLeft/cityRight/hotSets),直接数组索引而非 filter
粒子系统 14 粒子,🧱🧩✨🟨🟦 五图标循环(黄块蓝块对应乐高经典配色),独立 speed 0.6~2.0(所有应用中最快),setInterval 80ms(所有应用中最快刷新)水平向右飘动(x+speed,x>360回-20循环,模拟积木传送带/乐高工厂出件口),x 用 px 而非百分比,y 固定 5 行分布 80+(i%5)*118,所有应用中唯二水平飘动粒子(与奶茶并列,图标更贴合乐高)
数据可视化 绝版 Tab 绝版稀有度柱状图(3 款,height 直接用 rarity+‘vp’,i===2 骑士城堡 100 分红高亮其他蓝,375 骑士城堡传世级登顶)+ 图鉴 Tab 收藏进度条(rateColor 三色填充 >=55绿/>=40黄/红)
场景差异化 6 大顶部 Tab 完全不同布局:全城(积木回血节亮黄banner+双列瀑布流左右列高度错落)/绝版(横滑大卡蓝到深蓝渐变+绝版稀有度柱状图骑士城堡100分红高亮+捡漏榜)/图纸(MOC搭建时间轴亮黄竖线连接+steps步骤列表带亮黄圆形序号)/人仔(三列格子9个人仔+人仔交换行情筛选id%2===0)/科技(旗舰超跑对决卡42115兰博基尼蓝渐变VS42083布加迪灰渐变+列表+侧栏统计物流好评率)/街景(收藏标签云6个COLLECTIONS+紧凑列表rarityColor标签+集齐街景三杰彩蛋)
模态弹窗 4 个:发布出售(蓝渐变头,选套装/成色5档/价格步进器步长10 10-9999 >2000包邮建议/描述)/发起交换(亮黄头唯一浅色头,我提供/我想换多选pickedWant辅助方法i%2===0/面交地点5个/面交安全提示)/下架(🗑️小卡,留着文案/确认下架乐高红,可重新上架可逆操作)/套装详情(亮黄渐变头,四列统计售价红/零件数蓝/稀有度rarityColor/年份橙,宝贝描述+想换栏浅红底+圈友点评3条,留言问问+立即购买)
状态变量 19 个 @State + 1 个 private timerId,含 pickedPlaces 布尔数组实现多选
辅助方法 pickedWant(i%2===0) + toggleWant(设置swapWantIdx=i),用于发起交换弹窗多选
生命周期 aboutToAppear 构建粒子+启动定时器80ms,aboutToDisappear clearInterval+重置-1
头部特色 蓝三色渐变 angle:135 #0057A7→#1E88E5→#42A5F5(乐高蓝到亮蓝到浅蓝,模拟乐高积木渐变)+ 🔎📦🛍️三图标+购物车3角标亮黄底+搜索条+我的求购按钮亮黄底
底部 Tab 特色 纯白底 + 顶部圆角 18(与头部底部圆角呼应,乐高积木视觉隐喻)
顶部 Tab 特色 6 Tab 等宽,纯白底圆角14,激活态亮黄圆角下划线+非激活浅蓝底圆角下划线(双下划线对比)
分类色系统 catColor 6 色(套装类型映射,绝版红/MOC紫/人仔绿/科技橙红/街景亮蓝/经典乐高蓝)+ rarityColor 4 色(稀有度映射,>=95红/>=80橙/>=60蓝/绿)+ rateColor 3 色(收藏进度映射,>=55绿/>=40黄/红)
乐高圈文化梗 出闲置收绝版换心头好/骨灰级砖友/AFOL评测局/砖块医生/科技件玩家/传世级/绝版猎人/人仔百夫长/MOC搬运工/街景三杰/集齐彩蛋/积木回血节/卖家免服务费/面交安全提示/可重新上架/信用分986/累计成交额¥8.6万
特色数据字段 BrickItem.eng(英文名)+ BrickItem.parts(零件数)+ BrickItem.rarity(稀有度0-100)+ BrickItem.year(发行年份)+ BrickItem.want(卖家想换)+ MocItem.pages(页数)+ MocItem.downloads(下载量)+ MocItem.steps(步骤数组)+ FigItem.condition(成色)+ CollectionItem.owned/total/rate(收藏进度)+ SwapItem.offer/want/place(提供/想换/面交地点)
弹窗跳转 套装详情「留言问问」/「立即购买」→关闭详情

十四、完整代码

应用完整源码位于 1097.ets,总计约 1800 行,包含 9 个 interface、16 个常量数组/对象、7 个工具函数、9 个筛选函数、1 个 @Entry @Component struct Index 主组件(19 个 @State + 2 个生命周期方法 + 19 个 @Builder 方法 + 13 个 detail 查找方法 + 2 个辅助方法 + build 方法)。

核心代码结构如下:

// ============================================================
// 1097.ets - QQ乐高·积木回血社
// 场景:乐高套装/绝版/人仔/图纸的二手交易与交换社区(闲鱼风)
// 风格:乐高蓝 #0057A7 + 亮黄 #FFD400 明快乐高风
// 底部Tab:集市 / 交换 / 图鉴 / 我的
// 顶部Tab:全城 / 绝版 / 图纸 / 人仔 / 科技 / 街景
// 弹框:发布出售(新增) / 发起交换(新增) / 下架(删除) / 套装详情(详情)
// ============================================================

// ---------------- 配色 ----------------
interface ColorPalette {
  bg: string;
  card: string;
  blue: string;
  yellow: string;
  deep: string;
  red: string;
  green: string;
  sub: string;
  line: string;
}

const PALETTE: ColorPalette = {
  bg: '#F4F8FF',
  card: '#FFFFFF',
  blue: '#0057A7',
  yellow: '#FFD400',
  deep: '#123052',
  red: '#E8483C',
  green: '#00A25B',
  sub: '#7A8FA6',
  line: '#E4EDF8'
};

// ---------------- 通用结构 ----------------
interface TabItem {
  label: string;
  icon: string;
}

interface ParticleItem {
  x: number;
  emoji: string;
  size: number;
  speed: number;
  opacity: number;
}

// ---------------- 套装条目 ----------------
interface BrickItem {
  id: number;
  name: string;
  eng: string;
  cat: string;
  price: number;
  parts: number;
  rarity: number;
  year: number;
  rating: number;
  starStr: string;
  seller: string;
  tag: string;
  desc: string;
  want: string;
  sales: number;
  emoji: string;
}

// ---------------- 交换帖 ----------------
interface SwapItem {
  id: number;
  user: string;
  avatar: string;
  offer: string;
  want: string;
  time: string;
  likes: number;
  content: string;
  place: string;
}

// ---------------- 图纸/MOC ----------------
interface MocItem {
  id: number;
  name: string;
  type: string;
  pages: number;
  author: string;
  likes: number;
  downloads: number;
  emoji: string;
  steps: string[];
}

// ---------------- 人仔条目 ----------------
interface FigItem {
  id: number;
  name: string;
  series: string;
  price: number;
  condition: string;
  seller: string;
  emoji: string;
  desc: string;
}

// ---------------- 图鉴收藏 ----------------
interface CollectionItem {
  id: number;
  name: string;
  series: string;
  owned: number;
  total: number;
  rate: number;
  emoji: string;
}

// ---------------- 点评条目 ----------------
interface SetReview {
  id: number;
  user: string;
  setName: string;
  starStr: string;
  content: string;
}

// ---------------- Tab 配置 ----------------
const BOTTOM_TABS: TabItem[] = [
  { label: '集市', icon: '🏪' },
  { label: '交换', icon: '🤝' },
  { label: '图鉴', icon: '📖' },
  { label: '我的', icon: '🧱' }
];

const TOP_TABS: TabItem[] = [
  { label: '全城', icon: '🏙️' },
  { label: '绝版', icon: '🏆' },
  { label: '图纸', icon: '📐' },
  { label: '人仔', icon: '🪖' },
  { label: '科技', icon: '⚙️' },
  { label: '街景', icon: '🏘️' }
];

// ---------------- 套装数据(18款写死) ----------------
const BRICKS: BrickItem[] = [
  { id: 1, name: '10197 消防局', eng: 'Fire Brigade 2009', cat: 'retired', price: 2800, parts: 2231, rarity: 96, year: 2009, rating: 4.9, starStr: '⭐⭐⭐⭐⭐', seller: '阿乐不熬夜', tag: '绝版', desc: '盒说全无损,砖块分袋收纳,市面稀有货,出手回血换套街景。', want: '换 10255 集会广场', sales: 3, emoji: '🚒' },
  { id: 2, name: '6289 红魔海盗船', eng: 'Red Beard Runner', cat: 'retired', price: 5600, parts: 1730, rarity: 99, year: 1993, rating: 5.0, starStr: '⭐⭐⭐⭐⭐', seller: '帆船收藏家老王', tag: '绝版神物', desc: '90年代经典帆船,含完整船帆贴纸,缺两片小旗,收藏向出。', want: '诚心换 千年隼', sales: 1, emoji: '⛵' },
  { id: 3, name: '375 骑士城堡', eng: 'Yellow Castle 1978', cat: 'retired', price: 3200, parts: 488, rarity: 100, year: 1978, rating: 4.9, starStr: '⭐⭐⭐⭐⭐', seller: '复古砖块研究所', tag: '初代神作', desc: '乐高城堡系列开山之作,砖块均有年份刻印,情怀无价。', want: '面交优先', sales: 2, emoji: '🏰' },
  { id: 4, name: 'MOC-88213 樱花城堡', eng: 'Sakura Castle MOC', cat: 'moc', price: 48, parts: 3200, rarity: 35, year: 2025, rating: 4.8, starStr: '⭐⭐⭐⭐✨', seller: '图纸铺子', tag: '热门图纸', desc: '含PDF图纸+零件表+搭建视频,樱花配色绝美,手机就能看。', want: '可换人仔', sales: 126, emoji: '🌸' },
  { id: 5, name: 'MOC-11045 蒸汽飞艇', eng: 'Steampunk Airship', cat: 'moc', price: 66, parts: 4100, rarity: 40, year: 2024, rating: 4.7, starStr: '⭐⭐⭐⭐', seller: '蒸汽动力站', tag: '蒸汽朋克', desc: '全齿轮联动可动设计,含演示视频,零件清单已按颜色分组。', want: '换科技件', sales: 98, emoji: '🎈' },
  { id: 6, name: 'MOC-20331 四合院', eng: 'Chinese Courtyard', cat: 'moc', price: 89, parts: 5200, rarity: 28, year: 2025, rating: 4.9, starStr: '⭐⭐⭐⭐⭐', seller: '国风砖筑', tag: '国风限定', desc: '1:1 老北京四合院布局,含垂花门/影壁/回廊,细节爆炸。', want: '换街景套装', sales: 210, emoji: '🏮' },
  { id: 7, name: '星战白兵 1999初版', eng: 'Stormtrooper 1999', cat: 'fig', price: 450, parts: 5, rarity: 88, year: 1999, rating: 4.6, starStr: '⭐⭐⭐⭐', seller: '人仔贩子老白', tag: '初版白兵', desc: '初版白兵人仔,头盔印刷完好,少量把玩痕迹,无开裂。', want: '换克隆兵', sales: 8, emoji: '🥷' },
  { id: 8, name: '哈利·波特 斯内普', eng: 'Snape Minifigure', cat: 'fig', price: 320, parts: 4, rarity: 75, year: 2018, rating: 4.7, starStr: '⭐⭐⭐⭐', seller: '霍格沃茨快递', tag: '校长黑发版', desc: '斯内普教授人仔,黑色长袍+银蛇胸针,头发无裂。', want: '换邓布利多', sales: 15, emoji: '🧙' },
  { id: 9, name: '蝙蝠侠 2012版', eng: 'Batman 2012', cat: 'fig', price: 280, parts: 4, rarity: 70, year: 2012, rating: 4.5, starStr: '⭐⭐⭐⭐', seller: '哥谭夜行者', tag: '披风完整', desc: '深蓝战衣版蝙蝠侠,披风完好无破损,附底座。', want: '换小丑', sales: 22, emoji: '🦇' },
  { id: 10, name: '42115 兰博基尼', eng: 'Lamborghini Sián', cat: 'tech', price: 3800, parts: 3696, rarity: 60, year: 2020, rating: 4.9, starStr: '⭐⭐⭐⭐⭐', seller: '机械师阿伟', tag: '旗舰超跑', desc: '8速序列波箱+可动V12,拼好展示件,无缺件,含原盒。', want: '换布加迪奇龙', sales: 6, emoji: '🏎️' },
  { id: 11, name: '42083 布加迪奇龙', eng: 'Bugatti Chiron', cat: 'tech', price: 3500, parts: 3599, rarity: 62, year: 2018, rating: 4.8, starStr: '⭐⭐⭐⭐✨', seller: '轮毂收藏家', tag: '机械艺术品', desc: '可动W16引擎+升降尾翼,成色9新,箱说全。', want: '换兰博基尼', sales: 5, emoji: '🚗' },
  { id: 12, name: '42146 利勃海尔', eng: 'Liebherr Crane', cat: 'tech', price: 1200, parts: 2883, rarity: 45, year: 2023, rating: 4.6, starStr: '⭐⭐⭐⭐', seller: '吊车达人', tag: '电动旗舰', desc: '电动伸缩臂+四履带转向,App控制,缺遥控器电池盖。', want: '换42115', sales: 9, emoji: '🏗️' },
  { id: 13, name: '10218 宠物商店', eng: 'Pet Shop', cat: 'street', price: 1500, parts: 2032, rarity: 55, year: 2011, rating: 4.8, starStr: '⭐⭐⭐⭐✨', seller: '街景一条龙', tag: '已绝版', desc: '街景三杰之一,含鹦鹉/小狗,房屋可分两栋单独摆放。', want: '换剧院', sales: 12, emoji: '🐕' },
  { id: 14, name: '10255 集会广场', eng: 'Assembly Square', cat: 'street', price: 2600, parts: 4002, rarity: 50, year: 2017, rating: 4.9, starStr: '⭐⭐⭐⭐⭐', seller: '广场看守人', tag: '街景旗舰', desc: '10周年纪念款,含面包店/咖啡店/舞蹈室三层,细节拉满。', want: '换10278', sales: 7, emoji: '⛲' },
  { id: 15, name: '10278 警察局', eng: 'Police Station', cat: 'street', price: 1800, parts: 2923, rarity: 42, year: 2021, rating: 4.7, starStr: '⭐⭐⭐⭐', seller: '警长汤姆', tag: '双面墙体', desc: '含警车+监狱+甜甜圈店,楼层可拆,附经典蓝灯。', want: '换城市广场', sales: 11, emoji: '🚓' },
  { id: 16, name: '10305 狮王城堡', eng: 'Lion Knights Castle', cat: 'classic', price: 2900, parts: 4514, rarity: 30, year: 2022, rating: 4.9, starStr: '⭐⭐⭐⭐⭐', seller: '骑士团团长', tag: '周年大作', desc: '90周年城堡,可开合城门+升降吊桥+地牢,人仔22个。', want: '换黑鹰城堡', sales: 18, emoji: '🦁' },
  { id: 17, name: '21318 老友记公寓', eng: 'Central Perk', cat: 'classic', price: 700, parts: 1070, rarity: 65, year: 2019, rating: 4.8, starStr: '⭐⭐⭐⭐✨', seller: '六人行铁粉', tag: 'Ideas系列', desc: 'Central Perk咖啡厅,含六人主角人仔,橙色沙发是灵魂。', want: '换21335', sales: 34, emoji: '☕' },
  { id: 18, name: '21335 电动灯塔', eng: 'Motorized Lighthouse', cat: 'classic', price: 900, parts: 2065, rarity: 58, year: 2022, rating: 4.7, starStr: '⭐⭐⭐⭐', seller: '守塔人小陈', tag: '可亮灯', desc: '灯塔灯罩可旋转发光,含守塔人+救生艇,放书架超带感。', want: '换21318', sales: 26, emoji: '🗼' }
];

// ---------------- 交换帖数据(8条写死) ----------------
const SWAPS: SwapItem[] = [
  { id: 1, user: '乐高搬运工', avatar: '🧱', offer: '42083 布加迪', want: '42115 兰博基尼', time: '今天 13:20', likes: 46, content: '全新未拼,盒损而已,想平换兰博基尼,面交光谷地铁站。', place: '光谷广场站' },
  { id: 2, user: 'AFOL老张', avatar: '🛠️', offer: '10305 狮王城堡', want: '10197 消防局', time: '今天 11:05', likes: 38, content: '城堡拼完展示半年,砖块无白痕,想回血消防局,可补差价。', place: '江汉路站' },
  { id: 3, user: '小人仔批发商', avatar: '🪖', offer: '白兵×3 + 克隆兵×2', want: '斯内普教授', time: '昨天 18:42', likes: 61, content: '人仔全部验过,无开裂,想换斯内普,多换一好商量。', place: '洪山广场站' },
  { id: 4, user: '国风砖筑', avatar: '🏮', offer: 'MOC四合院图纸', want: '任何街景套装', time: '昨天 15:30', likes: 29, content: '图纸免费分享,想换实体街景,有10255优先。', place: '线上交换' },
  { id: 5, user: '科技控小马', avatar: '⚙️', offer: '42146 起重机', want: '42115 兰博基尼', time: '前天 20:15', likes: 22, content: '起重机少个电池盖,其他完好,加500差价换兰博基尼。', place: '楚河汉街站' },
  { id: 6, user: '街景集邮家', avatar: '🏘️', offer: '10218 宠物商店', want: '10255 集会广场', time: '前天 09:50', likes: 41, content: '宠物商店含鹦鹉,换集会广场,可包邮互换。', place: '线上互换' },
  { id: 7, user: '初代情怀党', avatar: '📜', offer: '375 骑士城堡', want: '6289 红魔海盗船', time: '3天前 22:08', likes: 55, content: '情怀物换情怀物,都是绝版老件,愿出价补差价。', place: '武昌火车站' },
  { id: 8, user: '图纸搬运工', avatar: '📐', offer: '樱花城堡+MOC手册', want: '任意人仔3个', time: '4天前 14:26', likes: 17, content: '图纸+整理好的零件表打包换人仔,学生党优先。', place: '线上交换' }
];

// ---------------- MOC图纸数据(5条写死) ----------------
const MOCS: MocItem[] = [
  { id: 1, name: '樱花城堡搭建手册', type: '建筑', pages: 86, author: '图纸铺子', likes: 342, downloads: 2100, emoji: '🌸', steps: ['地基与围墙模块 1-12页', '主体塔楼结构 13-38页', '樱花树与庭院 39-60页', '屋顶与飞檐 61-74页', '灯光与最终装配 75-86页'] },
  { id: 2, name: '蒸汽朋克飞艇图纸', type: '科技', pages: 64, author: '蒸汽动力站', likes: 276, downloads: 1580, emoji: '🎈', steps: ['动力舱齿轮组 1-18页', '气囊骨架搭建 19-34页', '甲板与舱室 35-50页', '螺旋桨联动 51-58页', '调试与展示 59-64页'] },
  { id: 3, name: '老北京四合院图册', type: '建筑', pages: 120, author: '国风砖筑', likes: 415, downloads: 3300, emoji: '🏮', steps: ['台基与回廊 1-28页', '垂花门结构 29-52页', '正房与厢房 53-80页', '影壁与花坛 81-98页', '瓦作收尾 99-120页'] },
  { id: 4, name: '复古小酒馆MOC', type: '街景', pages: 72, author: '街景一条龙', likes: 198, downloads: 1260, emoji: '🍺', steps: ['一层吧台区 1-22页', '二层雅座 23-46页', '招牌与霓虹 47-60页', '氛围灯饰 61-72页'] },
  { id: 5, name: '泰坦巨轮图纸', type: '舰船', pages: 95, author: '航海家老莫', likes: 233, downloads: 1420, emoji: '🚢', steps: ['龙骨与舱底 1-26页', '甲板分层 27-55页', '烟囱与船桥 56-78页', '舷窗与细节 79-95页'] }
];

// ---------------- 人仔数据(9条写死) ----------------
const FIGS: FigItem[] = [
  { id: 1, name: '星战白兵', series: '初版1999', price: 450, condition: '9成新', seller: '人仔贩子老白', emoji: '🥷', desc: '头盔印刷完整,无开裂' },
  { id: 2, name: '斯内普教授', series: '哈利波特', price: 320, condition: '9.5成新', seller: '霍格沃茨快递', emoji: '🧙', desc: '黑袍完整,银蛇胸针清晰' },
  { id: 3, name: '蝙蝠侠2012', series: 'DC', price: 280, condition: '8.5成新', seller: '哥谭夜行者', emoji: '🦇', desc: '披风完好,身体少量划痕' },
  { id: 4, name: '克隆人军官', series: '克隆战争', price: 260, condition: '全新', seller: '小人仔批发商', emoji: '🎖️', desc: '未拆袋,整盒拆出' },
  { id: 5, name: '哈利·波特', series: '哈利波特', price: 240, condition: '9成新', seller: '霍格沃茨快递', emoji: '⚡', desc: '眼镜完好,魔杖配件齐' },
  { id: 6, name: '达斯维达', series: '星战', price: 380, condition: '9成新', seller: '帝国军团', emoji: '🪖', desc: '黑甲漆面无损,光剑完好' },
  { id: 7, name: '印第安纳·琼斯', series: '夺宝奇兵', price: 350, condition: '8成新', seller: '探险家老K', emoji: '🤠', desc: '帽子有轻微压痕,鞭子完好' },
  { id: 8, name: '孙悟空人仔', series: '悟空小侠', price: 190, condition: '9.5成新', seller: '齐天大圣后援会', emoji: '🐵', desc: '金箍棒+筋斗云配件齐' },
  { id: 9, name: '马里奥人仔', series: '超级马力欧', price: 210, condition: '全新', seller: '管道工协会', emoji: '🍄', desc: '未拆封,含问号箱底座' }
];

// ---------------- 图鉴收藏数据(6条写死) ----------------
const COLLECTIONS: CollectionItem[] = [
  { id: 1, name: '街景系列', series: 'Modular Buildings', owned: 7, total: 18, rate: 39, emoji: '🏘️' },
  { id: 2, name: '科技旗舰', series: 'Technic Flagship', owned: 4, total: 9, rate: 44, emoji: '⚙️' },
  { id: 3, name: '城堡系列', series: 'Castle', owned: 5, total: 12, rate: 42, emoji: '🏰' },
  { id: 4, name: '星战载具', series: 'Star Wars Vehicles', owned: 9, total: 16, rate: 56, emoji: '🚀' },
  { id: 5, name: 'Ideas系列', series: 'LEGO Ideas', owned: 6, total: 10, rate: 60, emoji: '💡' },
  { id: 6, name: '哈利波特', series: 'Wizarding World', owned: 3, total: 8, rate: 38, emoji: '🧙' }
];

// ---------------- 点评数据(3条写死) ----------------
const SET_REVIEWS: SetReview[] = [
  { id: 1, user: 'AFOL评测局', setName: '10197 消防局', starStr: '⭐⭐⭐⭐⭐', content: '绝版街景的巅峰之作,砖块质量依旧在线,2009年的设计放到今天依然能打。' },
  { id: 2, user: '砖块医生', setName: 'MOC-20331 四合院', starStr: '⭐⭐⭐⭐✨', content: '图纸逻辑清晰,分步合理,零件表准确率高,国风细节控必入。' },
  { id: 3, user: '科技件玩家', setName: '42115 兰博基尼', starStr: '⭐⭐⭐⭐⭐', content: '旗舰科技的机械美学,波箱联动顺滑,摆柜效果无敌,价格坚挺有道理。' }
];

// ---------------- 全城周交易量图表数据 ----------------
const TRADE_LABELS: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const TRADE_BARS: number[] = [86, 64, 72, 58, 102, 156, 138];

// ---------------- 弹框选项 ----------------
const CONDITION_OPTIONS: string[] = ['全新未拆', '仅拆盒', '9成新', '8成新', '有缺件'];
const PRICE_LEVELS: string[] = ['捡漏价', '市场价', '收藏价'];
const PLACES: string[] = ['光谷广场站', '江汉路站', '洪山广场站', '楚河汉街站', '线上互换'];

// ---------------- 全局纯函数 ----------------
function catColor(cat: string): string {
  if (cat === 'retired') {
    return '#E8483C';
  }
  if (cat === 'moc') {
    return '#7C4DFF';
  }
  if (cat === 'fig') {
    return '#00A25B';
  }
  if (cat === 'tech') {
    return '#E86A5B';
  }
  if (cat === 'street') {
    return '#1E88E5';
  }
  return '#0057A7';
}

function catLabel(cat: string): string {
  if (cat === 'retired') {
    return '绝版回血';
  }
  if (cat === 'moc') {
    return '图纸分享';
  }
  if (cat === 'fig') {
    return '人仔交易';
  }
  if (cat === 'tech') {
    return '科技旗舰';
  }
  if (cat === 'street') {
    return '街景收藏';
  }
  return '经典好价';
}

function rarityLabel(r: number): string {
  if (r >= 95) {
    return '传世级';
  }
  if (r >= 80) {
    return '稀有级';
  }
  if (r >= 60) {
    return '紧俏级';
  }
  return '流通级';
}

function rarityColor(r: number): string {
  if (r >= 95) {
    return '#E8483C';
  }
  if (r >= 80) {
    return '#FF8A00';
  }
  if (r >= 60) {
    return '#1E88E5';
  }
  return '#00A25B';
}

function tradeBarHeight(v: number): string {
  return Math.round(v / 156 * 96).toString() + 'vp';
}

function rateColor(r: number): string {
  if (r >= 55) {
    return '#00A25B';
  }
  if (r >= 40) {
    return '#FFD400';
  }
  return '#E8483C';
}

function retiredSets(): BrickItem[] {
  return [BRICKS[0], BRICKS[1], BRICKS[2]];
}

function mocSets(): BrickItem[] {
  return [BRICKS[3], BRICKS[4], BRICKS[5]];
}

function figSets(): BrickItem[] {
  return [BRICKS[6], BRICKS[7], BRICKS[8]];
}

function techSets(): BrickItem[] {
  return [BRICKS[9], BRICKS[10], BRICKS[11]];
}

function streetSets(): BrickItem[] {
  return [BRICKS[12], BRICKS[13], BRICKS[14]];
}

function classicSets(): BrickItem[] {
  return [BRICKS[15], BRICKS[16], BRICKS[17]];
}

function cityLeft(): BrickItem[] {
  return [BRICKS[16], BRICKS[17]];
}

function cityRight(): BrickItem[] {
  return [BRICKS[15]];
}

function hotSets(): BrickItem[] {
  return [BRICKS[3], BRICKS[5], BRICKS[16], BRICKS[17], BRICKS[9]];
}

function swapBadge(likes: number): string {
  if (likes >= 50) {
    return '🔥热门';
  }
  if (likes >= 30) {
    return '👍热度';
  }
  return '🌱新帖';
}

// ============================================================
// 主页面
// ============================================================
@Entry
@Component
struct Index {
  @State currentBottomTab: number = 0;
  @State currentTopTab: number = 0;
  @State selectedBrickId: number = 1;
  @State showSellModal: boolean = false;
  @State showSwapModal: boolean = false;
  @State showDeleteModal: boolean = false;
  @State showDetailModal: boolean = false;
  @State sellBrickIdx: number = 0;
  @State sellCondIdx: number = 2;
  @State sellPrice: number = 100;
  @State sellWords: string = '';
  @State swapOfferIdx: number = 0;
  @State swapWantIdx: number = 1;
  @State pickedPlaces: boolean[] = [true, false, false, false, false];
  @State swapPlaceIdx: number = 0;
  @State particles: ParticleItem[] = [];
  private timerId: number = -1;

  aboutToAppear(): void {
    this.buildParticles();
    this.timerId = setInterval(() => {
      this.driftParticles();
    }, 80);
  }

  aboutToDisappear(): void {
    if (this.timerId !== -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
  }

  buildParticles(): void {
    const emojis: string[] = ['🧱', '🧩', '✨', '🟨', '🟦'];
    const list: ParticleItem[] = [];
    for (let i = 0; i < 14; i++) {
      list.push({
        x: Math.random() * 340,
        emoji: emojis[i % emojis.length],
        size: 12 + Math.random() * 10,
        speed: 0.6 + Math.random() * 1.4,
        opacity: 0.2 + Math.random() * 0.4
      });
    }
    this.particles = list;
  }

  driftParticles(): void {
    for (let i = 0; i < this.particles.length; i++) {
      this.particles[i].x += this.particles[i].speed;
      if (this.particles[i].x > 360) {
        this.particles[i].x = -20;
      }
    }
    this.particles = this.particles.slice();
  }

  // 详情弹框数据取值
  detailName(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].name;
      }
    }
    return '';
  }

  detailEng(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].eng;
      }
    }
    return '';
  }

  detailEmoji(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].emoji;
      }
    }
    return '🧱';
  }

  detailPrice(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return '¥' + BRICKS[i].price.toString();
      }
    }
    return '';
  }

  detailParts(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].parts.toString() + ' pcs';
      }
    }
    return '';
  }

  detailRarity(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].rarity.toString();
      }
    }
    return '';
  }

  detailYear(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].year.toString();
      }
    }
    return '';
  }

  detailSeller(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].seller;
      }
    }
    return '';
  }

  detailTag(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].tag;
      }
    }
    return '';
  }

  detailStar(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].starStr;
      }
    }
    return '';
  }

  detailRating(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].rating.toFixed(1) + '分';
      }
    }
    return '';
  }

  detailDesc(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].desc;
      }
    }
    return '';
  }

  detailWant(): string {
    for (let i = 0; i < BRICKS.length; i++) {
      if (BRICKS[i].id === this.selectedBrickId) {
        return BRICKS[i].want;
      }
    }
    return '';
  }

  // ---------------- 头部(电商风,无动画) ----------------
  @Builder
  headerBuilder() {
    Column() {
      Row() {
        Column() {
          Text('🧱 积木回血社')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('出闲置 · 收绝版 · 换心头好')
            .fontSize(11)
            .fontColor('#DFF0FF')
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Row({ space: 14 }) {
          Text('🔎')
            .fontSize(20)
          Text('📦')
            .fontSize(20)
          Column() {
            Text('🛍️')
              .fontSize(20)
            Text('3')
              .fontSize(9)
              .fontColor('#123052')
              .backgroundColor('#FFD400')
              .borderRadius(6)
              .padding({ left: 4, right: 4 })
              .margin({ top: -6 })
          }
          .alignItems(HorizontalAlign.End)
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 10 })

      Row({ space: 8 }) {
        Text('🔍 搜索:绝版 / 人仔 / MOC图纸')
          .fontSize(12)
          .fontColor('#A8C8E8')
          .layoutWeight(1)
        Text('🔨 我的求购')
          .fontSize(11)
          .fontColor('#123052')
          .backgroundColor('#FFD400')
          .borderRadius(10)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
    }
    .linearGradient({
      angle: 135,
      colors: [['#0057A7', 0], ['#1E88E5', 0.7], ['#42A5F5', 1]]
    })
    .borderRadius({ bottomLeft: 18, bottomRight: 18 })
  }

  // ---------------- 顶部Tab(可点击切换) ----------------
  @Builder
  topTabsBuilder() {
    Row() {
      ForEach(TOP_TABS, (t: TabItem, idx: number) => {
        Column() {
          Text(t.icon)
            .fontSize(17)
          Text(t.label)
            .fontSize(12)
            .fontColor(this.currentTopTab === idx ? '#0057A7' : '#7A8FA6')
            .fontWeight(this.currentTopTab === idx ? FontWeight.Bold : FontWeight.Normal)
            .margin({ top: 2 })
          if (this.currentTopTab === idx) {
            Divider()
              .width(16)
              .strokeWidth(3)
              .color('#FFD400')
              .borderRadius(2)
              .margin({ top: 4 })
          } else {
            Divider()
              .width(16)
              .strokeWidth(3)
              .color('#F4F8FF')
              .borderRadius(2)
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 6 })
        .onClick(() => {
          this.currentTopTab = idx;
        })
      }, (t: TabItem, idx: number) => 'top' + idx.toString() + t.label)
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(14)
    .margin({ top: 10 })
  }

  // ---------------- 全城Tab:banner + 双列瀑布 ----------------
  @Builder
  cityContent() {
    Column({ space: 12 }) {
      // 运营banner
      Column() {
        Text('🧱 积木回血节 · 第2季')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
        Text('绝版专场 3天限时 · 卖家免服务费')
          .fontSize(12)
          .fontColor('#5B6B7F')
          .margin({ top: 6 })
        Row({ space: 10 }) {
          Text('🏆 绝版捡漏')
            .fontSize(11)
            .fontColor('#123052')
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          Text('📐 MOC图纸')
            .fontSize(11)
            .fontColor('#123052')
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        }
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(18)
      .alignItems(HorizontalAlign.Start)
      .linearGradient({
        angle: 120,
        colors: [['#FFD400', 0], ['#FFB300', 1]]
      })
      .borderRadius(16)

      Text('✨ 全城新上架')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
        .width('100%')

      // 双列瀑布流
      Row({ space: 10 }) {
        Column({ space: 10 }) {
          ForEach(cityLeft(), (t: BrickItem) => {
            Column() {
              Text(t.emoji)
                .fontSize(34)
                .margin({ top: 16 })
              Text(t.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
                .margin({ top: 8 })
              Text(t.eng)
                .fontSize(9)
                .fontColor('#7A8FA6')
                .margin({ top: 2 })
              Text(t.desc)
                .fontSize(11)
                .fontColor('#7A8FA6')
                .margin({ top: 8 })
                .maxLines(3)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row({ space: 6 }) {
                Text('¥' + t.price.toString())
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#E8483C')
                Text(t.tag)
                  .fontSize(9)
                  .fontColor('#0057A7')
                  .backgroundColor('#E4F0FB')
                  .borderRadius(8)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              }
              .margin({ top: 10, bottom: 14 })
            }
            .width('100%')
            .backgroundColor('#FFFFFF')
            .borderRadius(14)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 14, right: 14 })
            .onClick(() => {
              this.selectedBrickId = t.id;
              this.showDetailModal = true;
            })
          }, (t: BrickItem) => 'cl' + t.id.toString())
        }
        .layoutWeight(1)

        Column({ space: 10 }) {
          ForEach(cityRight(), (t: BrickItem) => {
            Column() {
              Text(t.emoji)
                .fontSize(34)
                .margin({ top: 24 })
              Text(t.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
                .margin({ top: 8 })
              Text(t.eng)
                .fontSize(9)
                .fontColor('#7A8FA6')
                .margin({ top: 2 })
              Text(t.desc)
                .fontSize(11)
                .fontColor('#7A8FA6')
                .margin({ top: 10 })
                .maxLines(4)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row({ space: 6 }) {
                Text('¥' + t.price.toString())
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#E8483C')
                Text(t.starStr)
                  .fontSize(10)
              }
              .margin({ top: 12, bottom: 20 })
            }
            .width('100%')
            .backgroundColor('#FFF8DC')
            .borderRadius(14)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 14, right: 14 })
            .onClick(() => {
              this.selectedBrickId = t.id;
              this.showDetailModal = true;
            })
          }, (t: BrickItem) => 'cr' + t.id.toString())
        }
        .layoutWeight(1)
      }
      .width('100%')
      .alignItems(VerticalAlign.Top)
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 10, bottom: 16 })
  }

  // ---------------- 绝版Tab:横滑大卡 + 绝版度柱状图 + 捡漏榜 ----------------
  @Builder
  retiredContent() {
    Column({ space: 14 }) {
      Text('🏆 绝版回血专区')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
        .width('100%')

      // 横滑大卡
      Scroll() {
        Row({ space: 12 }) {
          ForEach(retiredSets(), (t: BrickItem) => {
            Column() {
              Text(t.emoji)
                .fontSize(40)
                .margin({ top: 18 })
              Text(t.name)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .margin({ top: 8 })
              Text(t.eng + ' · ' + t.year.toString() + '年')
                .fontSize(9)
                .fontColor('#DFF0FF')
                .margin({ top: 4 })
              Row({ space: 6 }) {
                Text('¥' + t.price.toString())
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FFD400')
                Text(rarityLabel(t.rarity))
                  .fontSize(10)
                  .fontColor('#123052')
                  .backgroundColor('#FFD400')
                  .borderRadius(8)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              }
              .margin({ top: 10, bottom: 18 })
            }
            .width(160)
            .linearGradient({
              angle: 160,
              colors: [['#0057A7', 0], ['#123052', 1]]
            })
            .borderRadius(16)
            .alignItems(HorizontalAlign.Center)
            .onClick(() => {
              this.selectedBrickId = t.id;
              this.showDetailModal = true;
            })
          }, (t: BrickItem) => 'rt' + t.id.toString())
        }
        .padding({ left: 14, right: 14 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')

      // 绝版度柱状图
      Column() {
        Text('📈 绝版稀有度排行')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .width('100%')
        Row({ space: 6 }) {
          ForEach(retiredSets(), (t: BrickItem, i: number) => {
            Column({ space: 5 }) {
              Text(t.rarity.toString())
                .fontSize(9)
                .fontColor('#0057A7')
              Column()
                .width(34)
                .height((t.rarity).toString() + 'vp')
                .backgroundColor(i === 2 ? '#E8483C' : '#1E88E5')
                .borderRadius({ topLeft: 5, topRight: 5 })
              Text(t.year.toString() + '年')
                .fontSize(9)
                .fontColor('#7A8FA6')
            }
            .alignItems(HorizontalAlign.Center)
            .layoutWeight(1)
          }, (t: BrickItem, i: number) => 'rb' + i.toString() + t.id.toString())
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Bottom)
        .padding({ top: 12 })
        Text('375 骑士城堡以100分稀有度登顶,市面流通不足百套')
          .fontSize(10)
          .fontColor('#7A8FA6')
          .margin({ top: 8 })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding(14)
      .margin({ left: 14, right: 14 })

      // 捡漏榜
      Column() {
        Text('🕵️ 今日捡漏榜')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .width('100%')
          .margin({ bottom: 8 })
        ForEach(retiredSets(), (t: BrickItem, i: number) => {
          Row({ space: 10 }) {
            Text((i + 1).toString())
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(i === 0 ? '#E8483C' : '#7A8FA6')
              .width(20)
            Text(t.emoji)
              .fontSize(18)
            Column() {
              Text(t.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
              Text(rarityLabel(t.rarity) + ' · ' + t.want)
                .fontSize(10)
                .fontColor('#7A8FA6')
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('¥' + t.price.toString())
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E8483C')
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 })
          .onClick(() => {
            this.selectedBrickId = t.id;
            this.showDetailModal = true;
          })
        }, (t: BrickItem, i: number) => 'pck' + i.toString() + t.id.toString())
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding(14)
      .margin({ left: 14, right: 14, bottom: 16 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .padding({ top: 10 })
  }

  // ---------------- 图纸Tab:MOC时间轴 ----------------
  @Builder
  mocContent() {
    Column({ space: 0 }) {
      Text('📐 MOC图纸 · 搭建说明书')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
        .width('100%')
        .margin({ top: 10, bottom: 12 })

      ForEach(MOCS, (m: MocItem, i: number) => {
        Row({ space: 12 }) {
          Column({ space: 4 }) {
            Text(m.emoji)
              .fontSize(20)
            if (i < MOCS.length - 1) {
              Column()
                .width(2)
                .layoutWeight(1)
                .backgroundColor('#FFD400')
                .margin({ top: 4 })
            }
          }
          .width(40)
          .alignItems(HorizontalAlign.Center)

          Column({ space: 6 }) {
            Row({ space: 8 }) {
              Text(m.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
                .layoutWeight(1)
              Text('⬇ ' + m.downloads.toString())
                .fontSize(10)
                .fontColor('#0057A7')
            }
            .width('100%')
            Text('by ' + m.author + ' · ' + m.type + ' · ' + m.pages.toString() + '页')
              .fontSize(10)
              .fontColor('#7A8FA6')
            Column({ space: 5 }) {
              ForEach(m.steps, (s: string, si: number) => {
                Row({ space: 8 }) {
                  Text((si + 1).toString())
                    .fontSize(9)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#123052')
                    .backgroundColor('#FFD400')
                    .borderRadius(9)
                    .width(18)
                    .height(18)
                    .textAlign(TextAlign.Center)
                  Text(s)
                    .fontSize(10)
                    .fontColor('#4A5A6E')
                    .layoutWeight(1)
                }
                .width('100%')
              }, (s: string, si: number) => 'ms' + si.toString() + m.id.toString())
            }
            .width('100%')
            .backgroundColor('#F4F8FF')
            .borderRadius(10)
            .padding(10)
            Row({ space: 8 }) {
              Text('❤️ ' + m.likes.toString())
                .fontSize(10)
                .fontColor('#E8483C')
              Text(m.emoji + ' 图纸可下载')
                .fontSize(10)
                .fontColor('#0057A7')
                .layoutWeight(1)
                .textAlign(TextAlign.End)
            }
            .width('100%')
          }
          .layoutWeight(1)
          .backgroundColor(i === 1 ? '#FFF8DC' : '#FFFFFF')
          .borderRadius(14)
          .padding(12)
          .margin({ bottom: 14 })
        }
        .width('100%')
        .alignItems(VerticalAlign.Top)
      }, (m: MocItem, i: number) => 'moc' + i.toString() + m.id.toString())
    }
    .width('100%')
    .padding({ left: 14, right: 14, bottom: 16 })
  }

  // ---------------- 人仔Tab:三列格子 + 兑换建议 ----------------
  @Builder
  figContent() {
    Column({ space: 14 }) {
      Text('🪖 人仔交易 · 摆件战场')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
        .width('100%')

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(FIGS, (f: FigItem) => {
          Column() {
            Text(f.emoji)
              .fontSize(26)
              .margin({ top: 12 })
            Text(f.name)
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .margin({ top: 6 })
              .maxLines(1)
            Text(f.series)
              .fontSize(8)
              .fontColor('#7A8FA6')
              .margin({ top: 2 })
            Text('¥' + f.price.toString())
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E8483C')
              .margin({ top: 4, bottom: 12 })
          }
          .width('31%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .alignItems(HorizontalAlign.Center)
          .margin({ bottom: 8 })
          .onClick(() => {
            this.selectedBrickId = 7 + f.id % 3;
            this.showDetailModal = true;
          })
        }, (f: FigItem) => 'fg' + f.id.toString())
      }
      .width('100%')

      Column() {
        Text('🤝 人仔交换行情')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .width('100%')
          .margin({ bottom: 6 })
        ForEach(FIGS, (f: FigItem) => {
          if (f.id % 2 === 0) {
            Row({ space: 10 }) {
              Text(f.emoji)
                .fontSize(20)
              Column({ space: 2 }) {
                Text(f.name + ' · ' + f.condition)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#123052')
                Text(f.desc)
                  .fontSize(10)
                  .fontColor('#7A8FA6')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('¥' + f.price.toString())
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#00A25B')
            }
            .width('100%')
            .padding({ top: 8, bottom: 8 })
          }
        }, (f: FigItem) => 'fgx' + f.id.toString())
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding(14)
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 10, bottom: 16 })
  }

  // ---------------- 科技Tab:对决卡 + 侧栏统计 ----------------
  @Builder
  techContent() {
    Column({ space: 14 }) {
      Text('⚙️ 科技旗舰 · 机械对决')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
        .width('100%')

      // 对决卡
      Row({ space: 10 }) {
        Column() {
          Text('🏎️')
            .fontSize(34)
            .margin({ top: 14 })
          Text('42115 兰博基尼')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .margin({ top: 6 })
          Text('3696 pcs · 现价¥3800')
            .fontSize(10)
            .fontColor('#DFF0FF')
            .margin({ top: 4, bottom: 14 })
        }
        .layoutWeight(1)
        .linearGradient({
          angle: 150,
          colors: [['#0057A7', 0], ['#123052', 1]]
        })
        .borderRadius(14)
        .alignItems(HorizontalAlign.Center)

        Column() {
          Text('VS')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0057A7')
          Text('本周')
            .fontSize(9)
            .fontColor('#7A8FA6')
        }
        .alignItems(HorizontalAlign.Center)

        Column() {
          Text('🚗')
            .fontSize(34)
            .margin({ top: 14 })
          Text('42083 布加迪')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .margin({ top: 6 })
          Text('3599 pcs · 现价¥3500')
            .fontSize(10)
            .fontColor('#DFF0FF')
            .margin({ top: 4, bottom: 14 })
        }
        .layoutWeight(1)
        .linearGradient({
          angle: 150,
          colors: [['#7A8FA6', 0], ['#123052', 1]]
        })
        .borderRadius(14)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

      // 列表 + 侧栏统计
      Row({ space: 10 }) {
        Column({ space: 10 }) {
          ForEach(techSets(), (t: BrickItem) => {
            Row({ space: 10 }) {
              Text(t.emoji)
                .fontSize(22)
              Column({ space: 2 }) {
                Text(t.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#123052')
                Text(t.tag + ' · ' + t.parts.toString() + 'pcs')
                  .fontSize(10)
                  .fontColor('#7A8FA6')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('¥' + t.price.toString())
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E8483C')
            }
            .width('100%')
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .padding(12)
            .onClick(() => {
              this.selectedBrickId = t.id;
              this.showDetailModal = true;
            })
          }, (t: BrickItem) => 'tc' + t.id.toString())
        }
        .layoutWeight(1)

        // 侧栏统计
        Column({ space: 10 }) {
          Column({ space: 4 }) {
            Text('12')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#0057A7')
            Text('本周科技件成交')
              .fontSize(9)
              .fontColor('#7A8FA6')
          }
          .width('100%')
          .backgroundColor('#E4F0FB')
          .borderRadius(12)
          .padding({ top: 10, bottom: 10 })
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Text('3.2万')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF8A00')
            Text('科技件总交易额')
              .fontSize(9)
              .fontColor('#7A8FA6')
          }
          .width('100%')
          .backgroundColor('#FFF8DC')
          .borderRadius(12)
          .padding({ top: 10, bottom: 10 })
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Text('86%')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#00A25B')
            Text('物流好评率')
              .fontSize(9)
              .fontColor('#7A8FA6')
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding({ top: 10, bottom: 10 })
          .alignItems(HorizontalAlign.Center)
        }
        .width(86)
      }
      .width('100%')
      .alignItems(VerticalAlign.Top)
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 10, bottom: 16 })
  }

  // ---------------- 街景Tab:标签云 + 紧凑列表 ----------------
  @Builder
  streetContent() {
    Column({ space: 14 }) {
      Text('🏘️ 街景收藏 · 城市拼图')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
        .width('100%')

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
        ForEach(COLLECTIONS, (c: CollectionItem) => {
          Text(c.emoji + ' ' + c.name + ' ' + c.owned.toString() + '/' + c.total.toString())
            .fontSize(11)
            .fontColor('#123052')
            .backgroundColor('#E4F0FB')
            .borderRadius(12)
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .margin(4)
        }, (c: CollectionItem) => 'ccoll' + c.id.toString())
      }
      .width('100%')

      ForEach(streetSets(), (t: BrickItem) => {
        Row({ space: 12 }) {
          Text(t.emoji)
            .fontSize(24)
          Column({ space: 2 }) {
            Row({ space: 6 }) {
              Text(t.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
              Text(rarityLabel(t.rarity))
                .fontSize(8)
                .fontColor('#FFFFFF')
                .backgroundColor(rarityColor(t.rarity))
                .borderRadius(6)
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
            }
            Text(t.desc)
              .fontSize(10)
              .fontColor('#7A8FA6')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('¥' + t.price.toString())
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#E8483C')
        }
        .width('100%')
        .backgroundColor(t.id === 15 ? '#E4F0FB' : '#FFFFFF')
        .borderRadius(12)
        .padding(12)
        .onClick(() => {
          this.selectedBrickId = t.id;
          this.showDetailModal = true;
        })
      }, (t: BrickItem) => 'st' + t.id.toString())

      Text('💬 集齐街景三杰可召唤隐藏彩蛋:宠物商店+集会广场+警察局')
        .fontSize(10)
        .fontColor('#7A8FA6')
        .width('100%')
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 10, bottom: 16 })
  }

  // ---------------- 集市首页(顶部Tab切换) ----------------
  @Builder
  bazaarContent() {
    Column() {
      this.topTabsBuilder()
      if (this.currentTopTab === 0) {
        this.cityContent()
      } else if (this.currentTopTab === 1) {
        this.retiredContent()
      } else if (this.currentTopTab === 2) {
        this.mocContent()
      } else if (this.currentTopTab === 3) {
        this.figContent()
      } else if (this.currentTopTab === 4) {
        this.techContent()
      } else {
        this.streetContent()
      }
    }
    .width('100%')
  }

  // ---------------- 交换页 ----------------
  @Builder
  swapContent() {
    Column({ space: 12 }) {
      // 统计banner
      Row({ space: 0 }) {
        Column({ space: 4 }) {
          Text('156')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0057A7')
          Text('在交换中')
            .fontSize(10)
            .fontColor('#7A8FA6')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)

        Column({ space: 4 }) {
          Text('42')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF8A00')
          Text('今日成交')
            .fontSize(10)
            .fontColor('#7A8FA6')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)

        Column({ space: 4 }) {
          Text('98%')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00A25B')
          Text('面交好评')
            .fontSize(10)
            .fontColor('#7A8FA6')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)

        Column({ space: 6 }) {
          Text('🤝 发交换')
            .fontSize(12)
            .fontColor('#FFFFFF')
            .backgroundColor('#FF8A00')
            .borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .onClick(() => {
              this.showSwapModal = true;
            })
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding({ top: 14, bottom: 14 })

      // 交换帖子列表
      ForEach(SWAPS, (s: SwapItem) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Text(s.avatar)
              .fontSize(26)
            Column({ space: 2 }) {
              Row({ space: 6 }) {
                Text(s.user)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#123052')
                Text(swapBadge(s.likes))
                  .fontSize(8)
                  .fontColor('#FF8A00')
                  .backgroundColor('#FFF8DC')
                  .borderRadius(6)
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
              }
              Text(s.time + ' · ' + s.place)
                .fontSize(9)
                .fontColor('#7A8FA6')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('⋯')
              .fontSize(16)
              .fontColor('#7A8FA6')
              .onClick(() => {
                this.showDeleteModal = true;
              })
          }
          .width('100%')

          // 交换内容
          Row({ space: 10 }) {
            Column({ space: 4 }) {
              Text('我提供')
                .fontSize(9)
                .fontColor('#0057A7')
              Text(s.offer)
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
            }
            .layoutWeight(1)
            .backgroundColor('#E4F0FB')
            .borderRadius(10)
            .padding(8)
            .alignItems(HorizontalAlign.Center)

            Text('⇄')
              .fontSize(14)
              .fontColor('#FFD400')

            Column({ space: 4 }) {
              Text('我想换')
                .fontSize(9)
                .fontColor('#E8483C')
              Text(s.want)
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
            }
            .layoutWeight(1)
            .backgroundColor('#FFEBEE')
            .borderRadius(10)
            .padding(8)
            .alignItems(HorizontalAlign.Center)
          }
          .width('100%')

          Text(s.content)
            .fontSize(11)
            .fontColor('#4A5A6E')
            .lineHeight(16)
            .width('100%')

          Row({ space: 8 }) {
            Text('📍 ' + s.place)
              .fontSize(10)
              .fontColor('#0057A7')
              .backgroundColor('#F4F8FF')
              .borderRadius(10)
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
            Text('❤️ ' + s.likes.toString())
              .fontSize(10)
              .fontColor('#E8483C')
              .layoutWeight(1)
              .textAlign(TextAlign.End)
          }
          .width('100%')
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
        .borderRadius(14)
        .padding(14)
      }, (s: SwapItem) => 'sw' + s.id.toString())
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 10, bottom: 16 })
  }

  // ---------------- 图鉴页 ----------------
  @Builder
  collectionContent() {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('📖 我的乐高图鉴')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#123052')
          Text('记录每一套心头好的收藏进度')
            .fontSize(10)
            .fontColor('#7A8FA6')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('✚ 登记')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#0057A7')
          .borderRadius(14)
          .padding({ left: 14, right: 14, top: 7, bottom: 7 })
          .onClick(() => {
            this.showSellModal = true;
          })
      }
      .width('100%')
      .margin({ top: 10 })

      ForEach(COLLECTIONS, (c: CollectionItem) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Text(c.emoji)
              .fontSize(28)
            Column({ space: 3 }) {
              Text(c.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
              Text(c.series + ' · 已收 ' + c.owned.toString() + '/' + c.total.toString())
                .fontSize(10)
                .fontColor('#7A8FA6')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(c.rate.toString() + '%')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(rateColor(c.rate))
          }
          .width('100%')

          Row({ space: 0 }) {
            Column()
              .width(c.rate.toString() + '%')
              .height(6)
              .backgroundColor(rateColor(c.rate))
              .borderRadius(3)
            Column()
              .width((100 - c.rate).toString() + '%')
              .height(6)
              .backgroundColor('#E4EDF8')
              .borderRadius(3)
          }
          .width('100%')
        }
        .width('100%')
        .backgroundColor(c.id === 5 ? '#FFF8DC' : '#FFFFFF')
        .borderRadius(14)
        .padding(14)
      }, (c: CollectionItem) => 'coll' + c.id.toString())

      Column() {
        Text('🏅 图鉴成就墙')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .width('100%')
          .margin({ bottom: 8 })
        Flex({ wrap: FlexWrap.Wrap }) {
          Text('🎖️ 街景三杰')
            .fontSize(10)
            .fontColor('#0057A7')
            .backgroundColor('#E4F0FB')
            .borderRadius(10)
            .padding({ left: 8, right: 8, top: 5, bottom: 5 })
            .margin(4)
          Text('🏆 绝版猎人')
            .fontSize(10)
            .fontColor('#E8483C')
            .backgroundColor('#FFEBEE')
            .borderRadius(10)
            .padding({ left: 8, right: 8, top: 5, bottom: 5 })
            .margin(4)
          Text('🪖 人仔百夫长')
            .fontSize(10)
            .fontColor('#FF8A00')
            .backgroundColor('#FFF8DC')
            .borderRadius(10)
            .padding({ left: 8, right: 8, top: 5, bottom: 5 })
            .margin(4)
          Text('📐 MOC搬运工')
            .fontSize(10)
            .fontColor('#00A25B')
            .backgroundColor('#E8F7EF')
            .borderRadius(10)
            .padding({ left: 8, right: 8, top: 5, bottom: 5 })
            .margin(4)
        }
        .width('100%')
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding(14)
    }
    .width('100%')
    .padding({ left: 14, right: 14, bottom: 16 })
  }

  // ---------------- 我的页 ----------------
  @Builder
  mineContent() {
    Column({ space: 14 }) {
      // 个人卡
      Column() {
        Row({ space: 12 }) {
          Text('🧱')
            .fontSize(40)
          Column({ space: 4 }) {
            Text('积木收藏家阿乐')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('LV.9 骨灰级砖友 · 信用分 986')
              .fontSize(10)
              .fontColor('#DFF0FF')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('>')
            .fontSize(18)
            .fontColor('#DFF0FF')
        }
        .width('100%')

        Row({ space: 0 }) {
          Column({ space: 3 }) {
            Text('38')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFD400')
            Text('在售套装')
              .fontSize(9)
              .fontColor('#DFF0FF')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column({ space: 3 }) {
            Text('12')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFD400')
            Text('求购中')
              .fontSize(9)
              .fontColor('#DFF0FF')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column({ space: 3 }) {
            Text('¥8.6万')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFD400')
            Text('累计成交额')
              .fontSize(9)
              .fontColor('#DFF0FF')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('100%')
      .padding(18)
      .linearGradient({
        angle: 120,
        colors: [['#0057A7', 0], ['#1E88E5', 1]]
      })
      .borderRadius(16)
      .margin({ top: 10 })

      // 我的求购
      Column() {
        Text('🔨 我的求购清单')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .width('100%')
          .margin({ bottom: 6 })
        ForEach(BRICKS, (t: BrickItem) => {
          if (t.id === 13 || t.id === 16 || t.id === 18) {
            Row({ space: 10 }) {
              Text(t.emoji)
                .fontSize(20)
              Column({ space: 2 }) {
                Text(t.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#123052')
                Text('预算 ¥' + t.price.toString() + ' 内 · ' + t.want)
                  .fontSize(9)
                  .fontColor('#7A8FA6')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('出价中')
                .fontSize(10)
                .fontColor('#FF8A00')
                .backgroundColor('#FFF8DC')
                .borderRadius(10)
                .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            }
            .width('100%')
            .padding({ top: 9, bottom: 9 })
          }
        }, (t: BrickItem) => 'wish' + t.id.toString())
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding(14)

      // 常去面交点
      Column() {
        Text('📍 常去面交点')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .width('100%')
          .margin({ bottom: 6 })
        ForEach(PLACES, (p: string, i: number) => {
          Row({ space: 10 }) {
            Text(['🚇', '🏬', '🚌', '🏪', '📦'][i])
              .fontSize(16)
            Text(p)
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .layoutWeight(1)
            Text(['光谷站台', '江汉路天桥', '洪山转盘', '楚河入口', '顺丰网点'][i])
              .fontSize(9)
              .fontColor('#7A8FA6')
          }
          .width('100%')
          .padding({ top: 9, bottom: 9 })
        }, (p: string, i: number) => 'pl' + i.toString() + p)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding(14)
    }
    .width('100%')
    .padding({ left: 14, right: 14, bottom: 16 })
  }

  // ---------------- 底部Tab ----------------
  @Builder
  bottomTabsBuilder() {
    Row() {
      ForEach(BOTTOM_TABS, (t: TabItem, idx: number) => {
        Column({ space: 3 }) {
          Text(t.icon)
            .fontSize(20)
          Text(t.label)
            .fontSize(10)
            .fontColor(this.currentBottomTab === idx ? '#0057A7' : '#7A8FA6')
            .fontWeight(this.currentBottomTab === idx ? FontWeight.Bold : FontWeight.Normal)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .onClick(() => {
          this.currentBottomTab = idx;
        })
      }, (t: TabItem, idx: number) => 'bt' + idx.toString() + t.label)
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius({ topLeft: 18, topRight: 18 })
  }

  // ---------------- 弹框1:发布出售(新增·蓝渐变头) ----------------
  @Builder
  sellModalBuilder() {
    Column() {
      // 弹框头
      Column() {
        Text('📦 发布出售')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .margin({ top: 16 })
        Text('闲置回血,绝版不愁卖')
          .fontSize(11)
          .fontColor('#DFF0FF')
          .margin({ top: 4, bottom: 16 })
      }
      .width('100%')
      .linearGradient({
        angle: 120,
        colors: [['#0057A7', 0], ['#1E88E5', 1]]
      })
      .borderRadius({ topLeft: 18, topRight: 18 })

      Scroll() {
        Column({ space: 14 }) {
          // 选套装
          Column({ space: 8 }) {
            Text('要出售的套装')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(BRICKS, (t: BrickItem, i: number) => {
                if (i < 8) {
                  Text(t.emoji + ' ' + t.name)
                    .fontSize(10)
                    .fontColor(this.sellBrickIdx === i ? '#FFFFFF' : '#123052')
                    .backgroundColor(this.sellBrickIdx === i ? '#0057A7' : '#E4F0FB')
                    .borderRadius(12)
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 })
                    .margin(4)
                    .onClick(() => {
                      this.sellBrickIdx = i;
                    })
                }
              }, (t: BrickItem, i: number) => 'st' + i.toString() + t.id.toString())
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 成色
          Column({ space: 8 }) {
            Text('成色说明')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(CONDITION_OPTIONS, (c: string, i: number) => {
                Text(c)
                  .fontSize(10)
                  .fontColor(this.sellCondIdx === i ? '#FFFFFF' : '#FF8A00')
                  .backgroundColor(this.sellCondIdx === i ? '#FF8A00' : '#FFF8DC')
                  .borderRadius(12)
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                  .margin(4)
                  .onClick(() => {
                    this.sellCondIdx = i;
                  })
              }, (c: string, i: number) => 'sd' + i.toString() + c)
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 价格步进器
          Row({ space: 12 }) {
            Text('出售价格')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
            Row({ space: 0 }) {
              Text('−')
                .fontSize(16)
                .fontColor('#0057A7')
                .backgroundColor('#E4F0FB')
                .borderRadius({ topLeft: 10, bottomLeft: 10 })
                .width(34)
                .height(30)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  if (this.sellPrice > 10) {
                    this.sellPrice -= 10;
                  }
                })
              Text('¥' + this.sellPrice.toString())
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#123052')
                .backgroundColor('#FFFFFF')
                .height(30)
                .textAlign(TextAlign.Center)
                .padding({ left: 14, right: 14 })
              Text('+')
                .fontSize(16)
                .fontColor('#FFFFFF')
                .backgroundColor('#0057A7')
                .borderRadius({ topRight: 10, bottomRight: 10 })
                .width(34)
                .height(30)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  if (this.sellPrice < 9999) {
                    this.sellPrice += 10;
                  }
                })
            }
            .borderRadius(10)

            Text(this.sellPrice > 2000 ? '💰 高价好物建议包邮' : '实惠价更容易出手')
              .fontSize(9)
              .fontColor(this.sellPrice > 2000 ? '#E8483C' : '#7A8FA6')
              .layoutWeight(1)
              .textAlign(TextAlign.End)
          }
          .width('100%')

          // 备注
          Column({ space: 8 }) {
            Text('宝贝描述')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            TextInput({ placeholder: '成色、缺件、配件情况如实描述…', text: this.sellWords })
              .fontSize(12)
              .fontColor('#123052')
              .backgroundColor('#F4F8FF')
              .borderRadius(10)
              .height(40)
              .padding({ left: 12, right: 12 })
              .onChange((v: string) => {
                this.sellWords = v;
              })
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 提交
          Row({ space: 12 }) {
            Text('取消')
              .fontSize(13)
              .fontColor('#7A8FA6')
              .backgroundColor('#F4F8FF')
              .borderRadius(20)
              .layoutWeight(1)
              .height(42)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showSellModal = false;
              })
            Text('上架出售')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .backgroundColor('#0057A7')
              .borderRadius(20)
              .layoutWeight(1)
              .height(42)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showSellModal = false;
              })
          }
          .width('100%')
          .margin({ top: 4, bottom: 18 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .align(Alignment.Top)
    }
    .width('88%')
    .height('76%')
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
    .clip(true)
  }

  // ---------------- 弹框2:发起交换(新增·亮黄头) ----------------
  @Builder
  swapModalBuilder() {
    Column() {
      Column() {
        Row({ space: 8 }) {
          Text('🤝')
            .fontSize(22)
          Text('发起交换')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#123052')
        }
        .margin({ top: 16 })
        Text('用闲置换心头好,砖友互助')
          .fontSize(11)
          .fontColor('#5B6B7F')
          .margin({ top: 4, bottom: 14 })
      }
      .width('100%')
      .backgroundColor('#FFD400')
      .borderRadius({ topLeft: 18, topRight: 18 })

      Scroll() {
        Column({ space: 14 }) {
          // 我提供
          Column({ space: 8 }) {
            Text('我提供')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(BRICKS, (t: BrickItem, i: number) => {
                if (i % 3 === 0 && i < 15) {
                  Text(t.emoji + ' ' + t.name)
                    .fontSize(10)
                    .fontColor(this.swapOfferIdx === i ? '#FFFFFF' : '#123052')
                    .backgroundColor(this.swapOfferIdx === i ? '#123052' : '#F4F8FF')
                    .borderRadius(12)
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 })
                    .margin(4)
                    .onClick(() => {
                      this.swapOfferIdx = i;
                    })
                }
              }, (t: BrickItem, i: number) => 'wo' + i.toString() + t.id.toString())
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 我想换
          Column({ space: 8 }) {
            Text('我想换(可多选)')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(BRICKS, (t: BrickItem, i: number) => {
                if (i % 3 === 2 && i < 15) {
                  Text((this.pickedWant(i) ? '✓ ' : '') + t.emoji + ' ' + t.name)
                    .fontSize(10)
                    .fontColor(this.pickedWant(i) ? '#FFFFFF' : '#E8483C')
                    .backgroundColor(this.pickedWant(i) ? '#E8483C' : '#FFEBEE')
                    .borderRadius(12)
                    .padding({ left: 9, right: 9, top: 5, bottom: 5 })
                    .margin(4)
                    .onClick(() => {
                      this.toggleWant(i);
                    })
                }
              }, (t: BrickItem, i: number) => 'ww' + i.toString() + t.id.toString())
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 面交地点
          Column({ space: 8 }) {
            Text('面交地点')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(PLACES, (p: string, i: number) => {
                Text(p)
                  .fontSize(10)
                  .fontColor(this.swapPlaceIdx === i ? '#FFFFFF' : '#0057A7')
                  .backgroundColor(this.swapPlaceIdx === i ? '#0057A7' : '#E4F0FB')
                  .borderRadius(12)
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                  .margin(4)
                  .onClick(() => {
                    this.swapPlaceIdx = i;
                  })
              }, (p: string, i: number) => 'wp' + i.toString() + p)
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 提示
          Row({ space: 8 }) {
            Text('💡')
              .fontSize(14)
            Text('交换双方可自行协商补差价,面交请选择人流密集的公共场所,注意安全。')
              .fontSize(10)
              .fontColor('#7A8FA6')
              .lineHeight(15)
              .layoutWeight(1)
          }
          .width('100%')
          .backgroundColor('#FFF8DC')
          .borderRadius(10)
          .padding(10)

          Row({ space: 12 }) {
            Text('再想想')
              .fontSize(13)
              .fontColor('#7A8FA6')
              .backgroundColor('#F4F8FF')
              .borderRadius(20)
              .layoutWeight(1)
              .height(42)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showSwapModal = false;
              })
            Text('发布交换帖')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .backgroundColor('#FFD400')
              .borderRadius(20)
              .layoutWeight(1)
              .height(42)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showSwapModal = false;
              })
          }
          .width('100%')
          .margin({ top: 4, bottom: 18 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .align(Alignment.Top)
    }
    .width('88%')
    .height('72%')
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
    .clip(true)
  }

  // ---------------- 弹框3:下架/删除(小警告) ----------------
  @Builder
  deleteModalBuilder() {
    Column({ space: 14 }) {
      Text('🗑️')
        .fontSize(36)
      Text('下架这条宝贝?')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#123052')
      Text('下架后买家将无法搜索到这条信息,\n收藏和留言会一并清空,可重新上架。')
        .fontSize(11)
        .fontColor('#7A8FA6')
        .textAlign(TextAlign.Center)
        .lineHeight(17)

      Row({ space: 12 }) {
        Text('留着')
          .fontSize(13)
          .fontColor('#0057A7')
          .backgroundColor('#E4F0FB')
          .borderRadius(20)
          .layoutWeight(1)
          .height(40)
          .textAlign(TextAlign.Center)
          .onClick(() => {
            this.showDeleteModal = false;
          })
        Text('确认下架')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#E8483C')
          .borderRadius(20)
          .layoutWeight(1)
          .height(40)
          .textAlign(TextAlign.Center)
          .onClick(() => {
            this.showDeleteModal = false;
          })
      }
      .width('100%')
      .margin({ top: 6 })
    }
    .width('72%')
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
    .padding({ left: 20, right: 20, top: 24, bottom: 22 })
    .alignItems(HorizontalAlign.Center)
  }

  // ---------------- 弹框4:套装详情(蓝黄渐变头) ----------------
  @Builder
  detailModalBuilder() {
    Column() {
      Column() {
        Text(this.detailEmoji())
          .fontSize(40)
          .margin({ top: 18 })
        Text(this.detailName())
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#123052')
          .margin({ top: 8 })
        Text(this.detailEng())
          .fontSize(10)
          .fontColor('#5B6B7F')
          .margin({ top: 2 })
        Row({ space: 8 }) {
          Text(this.detailTag())
            .fontSize(9)
            .fontColor('#FFFFFF')
            .backgroundColor('#0057A7')
            .borderRadius(8)
            .padding({ left: 7, right: 7, top: 2, bottom: 2 })
          Text(rarityLabel(Number(this.detailRarity())))
            .fontSize(9)
            .fontColor('#123052')
            .backgroundColor('#FFD400')
            .borderRadius(8)
            .padding({ left: 7, right: 7, top: 2, bottom: 2 })
        }
        .margin({ top: 10, bottom: 18 })
      }
      .width('100%')
      .linearGradient({
        angle: 135,
        colors: [['#FFD400', 0], ['#FFB300', 1]]
      })
      .borderRadius({ topLeft: 18, topRight: 18 })
      .alignItems(HorizontalAlign.Center)

      Scroll() {
        Column({ space: 14 }) {
          // 数据行
          Row({ space: 0 }) {
            Column({ space: 4 }) {
              Text(this.detailPrice())
                .fontSize(17)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E8483C')
              Text('售价')
                .fontSize(9)
                .fontColor('#7A8FA6')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 4 }) {
              Text(this.detailParts())
                .fontSize(17)
                .fontWeight(FontWeight.Bold)
                .fontColor('#0057A7')
              Text('零件数')
                .fontSize(9)
                .fontColor('#7A8FA6')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 4 }) {
              Text(this.detailRarity())
                .fontSize(17)
                .fontWeight(FontWeight.Bold)
                .fontColor(rarityColor(Number(this.detailRarity())))
              Text('稀有度')
                .fontSize(9)
                .fontColor('#7A8FA6')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 4 }) {
              Text(this.detailYear())
                .fontSize(17)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF8A00')
              Text('发行年份')
                .fontSize(9)
                .fontColor('#7A8FA6')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }
          .width('100%')
          .backgroundColor('#F4F8FF')
          .borderRadius(12)
          .padding({ top: 12, bottom: 12 })

          // 描述
          Column({ space: 8 }) {
            Text('🧾 宝贝描述')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            Text(this.detailDesc())
              .fontSize(11)
              .fontColor('#4A5A6E')
              .lineHeight(18)
            Row({ space: 8 }) {
              Text('👤 ' + this.detailSeller())
                .fontSize(10)
                .fontColor('#0057A7')
              Text(this.detailStar() + ' ' + this.detailRating())
                .fontSize(10)
                .fontColor('#FF8A00')
                .layoutWeight(1)
                .textAlign(TextAlign.End)
            }
            .width('100%')
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 想换
          Row({ space: 8 }) {
            Text('🤝')
              .fontSize(14)
            Text('卖家想换:' + this.detailWant())
              .fontSize(11)
              .fontColor('#E8483C')
              .layoutWeight(1)
          }
          .width('100%')
          .backgroundColor('#FFEBEE')
          .borderRadius(10)
          .padding(10)

          // 点评
          Column({ space: 10 }) {
            Text('💬 圈友点评')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#123052')
              .width('100%')
            ForEach(SET_REVIEWS, (r: SetReview) => {
              Column({ space: 5 }) {
                Row({ space: 8 }) {
                  Text('👤')
                    .fontSize(14)
                  Text(r.user)
                    .fontSize(11)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#123052')
                  Text(r.starStr)
                    .fontSize(9)
                    .layoutWeight(1)
                    .textAlign(TextAlign.End)
                }
                .width('100%')
                Text(r.content)
                  .fontSize(10)
                  .fontColor('#7A8FA6')
                  .lineHeight(15)
              }
              .width('100%')
              .backgroundColor('#F4F8FF')
              .borderRadius(10)
              .padding(10)
            }, (r: SetReview) => 'rv' + r.id.toString())
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)

          // 操作
          Row({ space: 12 }) {
            Text('💬 留言问问')
              .fontSize(13)
              .fontColor('#0057A7')
              .backgroundColor('#E4F0FB')
              .borderRadius(20)
              .layoutWeight(1)
              .height(42)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showDetailModal = false;
              })
            Text('🛒 立即购买')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .backgroundColor('#E8483C')
              .borderRadius(20)
              .layoutWeight(1)
              .height(42)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showDetailModal = false;
              })
          }
          .width('100%')
          .margin({ top: 2, bottom: 18 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 14 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .align(Alignment.Top)
    }
    .width('88%')
    .height('78%')
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
    .clip(true)
  }

  // 交换多选辅助
  pickedWant(i: number): boolean {
    return (i % 2) === 0;
  }

  toggleWant(i: number): void {
    this.swapWantIdx = i;
  }

  // ---------------- 主布局 ----------------
  build() {
    Stack() {
      // 主页面
      Column() {
        this.headerBuilder()
        Scroll() {
          Column() {
            if (this.currentBottomTab === 0) {
              this.bazaarContent()
            } else if (this.currentBottomTab === 1) {
              this.swapContent()
            } else if (this.currentBottomTab === 2) {
              this.collectionContent()
            } else {
              this.mineContent()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        .align(Alignment.Top)

        this.bottomTabsBuilder()
      }
      .width('100%')
      .height('100%')

      // 粒子特效层
      ForEach(this.particles, (p: ParticleItem, i: number) => {
        Text(p.emoji)
          .fontSize(p.size)
          .opacity(p.opacity)
          .position({ x: p.x, y: 80 + (i % 5) * 118 })
      }, (p: ParticleItem, i: number) => 'pt' + i.toString())

      // 弹框4:套装详情
      if (this.showDetailModal) {
        Column() {
          this.detailModalBuilder()
        }
        .width('100%')
        .height('100%')
        .backgroundColor('99000000')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showDetailModal = false;
        })
      }

      // 弹框1:发布出售
      if (this.showSellModal) {
        Column() {
          this.sellModalBuilder()
        }
        .width('100%')
        .height('100%')
        .backgroundColor('99000000')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showSellModal = false;
        })
      }

      // 弹框2:发起交换
      if (this.showSwapModal) {
        Column() {
          this.swapModalBuilder()
        }
        .width('100%')
        .height('100%')
        .backgroundColor('99000000')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showSwapModal = false;
        })
      }

      // 弹框3:下架
      if (this.showDeleteModal) {
        Column() {
          this.deleteModalBuilder()
        }
        .width('100%')
        .height('100%')
        .backgroundColor('99000000')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showDeleteModal = false;
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(PALETTE.bg)
  }
}

1097.ets 放入 HarmonyOS 项目的 entry/src/main/ets/pages/ 目录下,在 main_pages.json 中注册路由,即可在 DevEco Studio 中预览运行。

十五、开发环境搭建:DevEco Studio + API 24

本应用基于 HarmonyOS NEXT(API 24)开发,使用 ArkTS 声明式 UI 框架。以下是完整的开发环境搭建步骤。

15.1 安装 DevEco Studio

从华为开发者联盟官网下载 DevEco Studio 安装包,双击运行安装程序。安装欢迎界面如下,点击「Next」继续。

DevEco Studio安装欢迎界面

15.2 选择安装位置

在安装路径选择界面,建议选择空间充足的磁盘(建议剩余空间 20GB 以上),避免使用中文路径或含空格的路径。点击「Next」继续。

选择DevEco Studio安装路径

15.3 配置安装选项

在安装选项配置界面,勾选「Create Desktop Shortcut」(创建桌面快捷方式)、「Add to PATH」(添加到环境变量)等选项,方便后续命令行调用。点击「Next」继续。

DevEco Studio安装选项配置

15.4 新建项目:选择 Empty Ability 模板

安装完成后启动 DevEco Studio,点击「Create Project」新建项目。在模板选择界面,选择「Empty Ability」(空 Ability)模板,这是最基础的 ArkTS 应用模板,适合从零构建自定义 UI。点击「Next」继续。

选择Empty Ability模板

15.5 配置项目信息:API 24

在项目配置界面,填写项目名称(如 QQLegoBrick)、包名(如 com.example.qqlegobrick),Compatible SDK 选择 6.1.1(24)(即 API 24),Save location 选择项目保存路径。确认信息无误后点击「Finish」创建项目。

配置项目信息与API 24

15.6 导入源码与运行

项目创建完成后,将 1097.ets 文件复制到 entry/src/main/ets/pages/ 目录下,打开 entry/src/main/resources/base/profile/main_pages.json,在 src 数组中添加 "pages/1097" 路由注册。

在 DevEco Studio 顶部工具栏选择 Previewer(预览器)或连接真机/模拟器,点击运行按钮即可看到「QQ乐高·积木回血社」应用的完整运行效果。可以切换底部 4 Tab、顶部 6 Tab,点击套装卡片查看详情、在集市发布出售、在交换页发起交换、在打卡动态中下架宝贝,体验完整的乐高二手交易与交换闭环。

十六、总结

「QQ乐高·积木回血社」是一个以乐高为主题的完整移动端二手交易与交换社区应用,基于 HarmonyOS NEXT(API 24)ArkTS 声明式 UI 框架构建,单文件 1097.ets 约 1800 行代码实现了全部功能。

应用的核心亮点包括:

  1. 乐高蓝 + 亮黄 + 乐高红明快乐高风视觉体系:10 色基础调色板 + catColor 6 色套装类型映射(绝版乐高红/MOC 紫/人仔乐高绿/科技橙红/街景亮蓝/经典乐高蓝)+ rarityColor 4 色稀有度映射(>=95 传世级红/>=80 稀有级橙/>=60 紧俏级蓝/流通级绿)+ rateColor 3 色收藏进度映射(>=55 绿/>=40 黄/红),通过色彩快速区分套装类型与稀有度。头部使用 angle:135 蓝三色渐变 #0057A7→#1E88E5→#42A5F5(从乐高蓝到亮蓝到浅蓝,模拟乐高积木从深蓝到浅蓝的渐变层次)+ 🔎📦🛍️三图标 + 购物车 3 角标(亮黄底)+ 搜索条 + 我的求购按钮,完整模拟了电商/二手交易 App 的头部交互体验。底部 Tab 纯白底 + 顶部圆角 18(与头部底部圆角呼应,乐高积木视觉隐喻)。
  2. 双层导航 + 6 大集市差异化布局:底部 4 Tab(集市/交换/图鉴/我的)+ 顶部 6 Tab(全城/绝版/图纸/人仔/科技/街景),6 个套装类型采用完全不同的布局风格(全城亮黄 banner+双列瀑布流左右列高度错落/绝版横滑大卡蓝到深蓝渐变+绝版稀有度柱状图骑士城堡 100 分红高亮+捡漏榜/图纸 MOC 搭建时间轴亮黄竖线连接+steps 步骤列表带亮黄圆形序号/人仔三列格子 9 个人仔+人仔交换行情筛选 id%2===0/科技旗舰超跑对决卡 42115 兰博基尼蓝渐变 VS 42083 布加迪灰渐变+列表+侧栏统计物流好评率/街景收藏标签云 6 个 COLLECTIONS+紧凑列表 rarityColor 标签+集齐街景三杰彩蛋),是乐高差异化设计的典范。顶部 Tab 激活态亮黄圆角下划线 + 非激活浅蓝底圆角下划线(双下划线对比),是独特的视觉细节。
  3. 14 粒子最快速度水平向右飘动动画:🧱🧩✨🟨🟦 五图标循环(黄块蓝块直接对应乐高经典配色),每个粒子拥有独立 speed 0.6~2.0(所有应用中最快)水平向右飘动(x+speed,x>360 回 -20 循环,模拟积木在传送带上向右移动、乐高工厂出件口一块接一块送出的场景,与垂直飘动的常规粒子形成鲜明反差,是所有应用中唯二采用水平飘动粒子的应用,与奶茶应用并列但图标更贴合乐高主题)+ 左侧循环,x 用 px 而非百分比,y 固定 5 行分布 80+(i%5)*118,setInterval 80ms(所有应用中最快刷新频率) 驱动(配合最快速度模拟乐高工厂出件的快节奏与乐高爱好者的拼搭快乐),this.particles = this.particles.slice() 通过 slice() 创建新引用触发状态更新,aboutToAppear/aboutToDisappear 生命周期管理。ParticleItem 无 id/y 字段(水平飘动专用),是本应用粒子系统最具特色的设计决策。
  4. 绝版稀有度柱状图 + 图鉴收藏进度条双数据可视化:绝版 Tab 内嵌绝版稀有度柱状图(3 款绝版,height 直接用 rarity+'vp' 即稀有度分数直接作为柱子高度,i===2 骑士城堡 100 分乐高红 #E8483C 高亮其他亮蓝,375 骑士城堡传世级登顶形成「市面流通不足百套」可视化,底部文字「375 骑士城堡以100分稀有度登顶」)+ 图鉴 Tab 收藏进度条(6 个图鉴收藏,rateColor 三色填充 >=55 乐高绿/>=40 亮黄/乐高红,进度条 width:rate% 填充),双数据可视化是本应用的特色。绝版稀有度柱状图直接用稀有度分数作为柱子高度,是所有应用中最直观的数据可视化设计。
  5. 4 个模态弹窗闭环:发布出售(蓝渐变头,选套装/成色 5 档/价格步进器步长 10 10-9999 >2000 给出「💰高价好物建议包邮」/宝贝描述)、发起交换(亮黄头,是所有弹窗中唯一用亮黄浅色头的,我提供/我想换多选 pickedWant 辅助方法 i%2===0/面交地点 5 个/面交安全提示「选择人流密集的公共场所注意安全」)、下架(🗑️小卡,「留着」轻松文案替代取消/确认下架乐高红,可重新上架可逆操作与其他应用的不可恢复删除不同,符合二手交易平台逻辑)、套装详情(亮黄渐变头,四列统计售价红/零件数蓝/稀有度 rarityColor/年份橙,宝贝描述+想换栏浅红底展示卖家以物换物意向+圈友点评 3 条充满乐高圈梗,留言问问+立即购买),覆盖新增/新增/删除/详情四态。
  6. 强类型数据模型 + 特色字段 + 工具函数 + 筛选函数三层架构9 个 interface(所有应用中最多)约束所有数据结构,BrickItem 17 字段(所有应用中最多,含 eng 英文名、parts 零件数、rarity 稀有度 0-100、year 发行年份、want 卖家想换五个特色字段),MocItem 含 pages 页数/downloads 下载量/steps 步骤数组,FigItem 含 condition 成色,CollectionItem 含 owned/total/rate 收藏进度,SwapItem 含 offer/want/place 提供/想换/面交地点,18 套装 + 8 交换帖 + 5 MOC 图纸 + 9 人仔 + 6 图鉴收藏 + 3 点评的丰富预置数据,7 个工具函数负责视觉映射,9 个筛选函数(直接数组索引而非 filter)负责数据分发,detail 系列 13 个查找方法(所有应用中最多)实现详情弹窗的数据驱动,2 个辅助方法(pickedWant/toggleWant)实现交换弹窗多选。三层架构让模板简洁、逻辑清晰。
  7. 乐高圈文化梗贯穿全应用:出闲置收绝版换心头好/骨灰级砖友/AFOL 评测局/砖块医生/科技件玩家/传世级/绝版猎人/人仔百夫长/MOC 搬运工/街景三杰/集齐彩蛋/积木回血节/卖家免服务费/面交安全提示/可重新上架/信用分 986/累计成交额 ¥8.6万/骑士城堡市面流通不足百套/街景三杰隐藏彩蛋,让应用充满乐高二手交易社区的文化认同感。「传世级」形容 95 分以上的绝版神物、「街景三杰」指宠物商店+集会广场+警察局、「AFOL」是 Adult Fan of LEGO 的缩写,这些梗只有真正的乐高爱好者才能会心一笑,是本应用的文化深度体现。

QQ乐高·积木回血社整体运行效果

对于希望学习 HarmonyOS ArkTS 声明式 UI 开发的读者,本应用提供了一个完整的参考范本:如何通过 @State 驱动状态更新、如何用 @Builder 拆分复杂布局、如何用 Stack 分层实现粒子与弹窗、如何用 setInterval + 生命周期实现动画(特别是如何通过方向反转——从垂直飘动改为水平飘动——模拟不同场景的运动意象,积木传送带的水平飘动与星空的静谧垂直飘动形成鲜明对比)、如何用 slice() 创建新引用触发数组状态更新、如何用 interface + 特色字段 + 常量数组 + 工具函数 + 筛选函数构建分层数据层、如何通过绝版稀有度柱状图(直接用稀有度分数作为柱子高度)+ 图鉴收藏进度条实现双数据可视化、如何通过蓝三色渐变头部 + 顶部圆角底部 Tab 营造明快乐高风、如何通过 6 Tab 差异化布局展示不同套装类型、如何通过 pickedWant 辅助方法实现弹窗多选、如何通过价格步进器 + 包邮建议实现二手交易定价、如何通过可重新上架的下架逻辑符合二手交易平台规范。将源码导入 DevEco Studio(API 24)即可直接运行预览,在此基础上可以扩展更多乐高套装数据、接入真实二手交易 API 与物流跟踪、增加乐高拍卖功能、增加交换匹配算法、增加砖友等级与信用系统、增加乐高真伪鉴定服务,打造一个真正可用的乐高二手交易与交换平台。

一砖一世界,绝版即永恒。愿每一个乐高爱好者,都能在绝版街景的深邃里回到童年拼搭的快乐,在初版白兵的信仰里找到星战迷的共鸣,在 MOC 图纸的步骤里搭出属于自己的世界,也能在积木与代码之间找到属于自己的那份明亮与快乐。出闲置,收绝版,换心头好——积木回血社,等你上架。

标签: HarmonyOS · ArkTS · API 24

Logo

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

更多推荐