引言

在这里插入图片描述

垂钓作为一项兼具休闲性和竞技性的户外运动,在中国拥有数千万爱好者的庞大群体。然而,传统渔具购买渠道分散、钓场信息不透明、装备租赁流程繁琐等问题长期困扰着钓鱼爱好者。本文将深度剖析一个名为"京东垂钓渔具馆"的综合性渔具电商应用,该应用集渔具销售、器材租赁、钓场预约、渔情速报和消息通知于一体,涵盖了渔具集市、鱼竿专区、鱼饵窝料、钓箱配件、钓场预约、消息中心和用户中心等七大功能模块,充分展示了 HarmonyOS 声明式 UI 框架在构建户外运动行业电商应用方面的强大能力。

从技术架构角度来看,该应用采用了鸿蒙生态中的 ArkTS 编程语言进行开发,基于 HarmonyOS ArkTS API 24 构建。应用通过接口定义了商品、标签和消息三种核心数据模型,通过 @Entry@Component 装饰器定义页面级组件,利用 @State 装饰器实现响应式状态管理。应用大量使用了 @Builder 装饰器封装可复用的 UI 构建方法,将复杂界面拆分为独立的构建单元。特别值得一提的是,应用在头部栏中集成了地理位置定位(杭州·西湖区)和实时渔情信息(温度、风力、鱼情),在钓场预约模块中提供了水深柱状图和渔情指数卡片,在鱼竿专区中通过硬度参数柱状图展示不同调性鱼竿的对比,这些功能充分体现了垂钓行业的专业特色。

在业务设计层面,该应用构建了"购-租-约-报"的完整渔具服务闭环。用户可以在渔具集市浏览精选渔具并查看爆护率和鱼获数据,在鱼竿专区通过硬度矩阵选择合适调性的鱼竿,在鱼饵窝料标签页查看库存排行和小药添加剂,在钓箱配件仓浏览钓箱、支架、夜钓灯等配套装备,在钓场预约模块选择钓场和时段并预约钓位。应用通过丰富的数据可视化组件(柱状图、进度条、排名序号、渔情指数等)和多种弹窗交互(发布闲置、渔具租赁、钓位预约、删除确认、商品详情)实现了完整的垂钓电商业务流程。

一、数据模型与接口定义

在这里插入图片描述

应用通过三个核心接口定义了数据模型的结构,为整个应用的数据流提供了类型安全保障。

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: boolean
}

interface TabInfo {
  name: string
  icon: string
}

interface MsgItem {
  title: string
  content: string
  time: string
  read: boolean
}

GoodsItem 是应用中最核心的数据接口,描述了渔具商品的完整信息:name 为商品名称,price 为价格,desc 为描述文案,score 为好评度百分比,num 在不同上下文中语义不同(在通用商品列表中为库存件数,在鱼竿列表中为硬度等级数字,在鱼饵列表中为库存包数,在钓箱列表中为容量规格,在钓场列表中为水深米数),tag 为标签文案,hot 为布尔型热门标识。TabInfo 定义了底部导航栏标签信息,包含名称和图标。MsgItem 描述消息通知数据结构,其中 read 布尔字段区分已读未读状态。这种多用途接口设计减少了接口数量,但也要求开发者在不同上下文中正确理解 num 字段的语义。例如在鱼竿专区中,num 值代表硬度等级(2H-8H),在钓场预约中则代表水深(1-12 米),这种同一字段在不同列表中承载不同业务含义的设计在垂直行业应用中较为常见。

二、组件状态声明与消息数据初始化

在这里插入图片描述

应用通过 @Entry@Component 装饰器定义页面级组件,通过大量 @State 变量管理应用状态,并预置了丰富的消息通知数据。

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showRent: boolean = false
  @State showPool: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State selType: number = 0
  @State selWater: number = 0
  @State selDay: number = 0
  @State rentDays: number = 3
  @State selPool: number = 0
  @State selSlot: number = 0
  @State personNum: number = 2
  @State deleteIndex: number = -1
  @State msgs: MsgItem[] = [
    { title: '渔情速报', content: '千岛湖今日鲫鱼开口好,浅滩钓位已放出 12 个', time: '11:02', read: false },
    { title: '租赁提醒', content: '您租的碳素竿明天到期,续租可免一天押金', time: '09:15', read: false },
    { title: '预约成功', content: '周六上午青龙湖 3 号钓位已锁定,请准时到场', time: '昨天', read: false },
    { title: '物流更新', content: '您的窝料套餐已从武汉仓发出,预计后天送达', time: '昨天', read: true },
    { title: '渔具保养', content: '竿节进水别硬拔,保养卡券已放入卡包', time: '前天', read: true },
    { title: '爆护榜单', content: '本周爆护榜已更新,钓友"老猫"以 46 斤夺冠', time: '前天', read: true },
    { title: '直播预告', content: '今晚 20:00 直拆新到的路亚饵,抽 20 份试用品', time: '周三', read: true },
    { title: '优惠券到账', content: '会员日渔具满 299 减 60 券已发放,7 天有效', time: '周三', read: true },
    { title: '钓场公告', content: '月湖钓场本周六举办夜钓赛,报名费 50/人', time: '周四', read: true },
    { title: '库存提醒', content: '您收藏的浮漂套装已补货,仅剩 8 套', time: '上周', read: true },
    { title: '评价有礼', content: '晒爆护图评价窝料可领 15 元券', time: '上周', read: true },
    { title: '安全提醒', content: '雷雨天气请勿在开阔水域垂钓,注意防雷', time: '上周', read: true }
  ]

状态变量分为三类:第一类控制 UI 展示状态,包括 currentTab(当前标签页索引)和五个弹窗显隐布尔变量(showPublishshowRentshowPoolshowDeleteshowDetail)。第二类记录用户在弹窗中的选择状态:selType(渔具类型选择)、selWater(适用水域选择)、selDay(租期套餐选择)、rentDays(租赁天数)、selPool(钓场选择)、selSlot(到钓时段选择)、personNum(同行人数)。第三类是数据传递变量:selectedItem 联合类型变量存储用户点击查看的商品对象,deleteIndex 记录待删除消息的索引。msgs 消息数组声明为 @State 以支持 splice 删除操作。消息数据涵盖了渔情速报、租赁提醒、预约成功、物流更新、渔具保养、爆护榜单、直播预告、优惠券、钓场公告、库存提醒、评价有礼和安全提醒等十二种垂钓场景通知,前三条为未读状态。

三、配置数据与商品集合初始化

在这里插入图片描述

应用预置了丰富的配置数据和五个商品数据集合,分别对应渔具集市、鱼竿专区、鱼饵窝料、钓箱配件和钓场预约五个标签页。

  private tabs: TabInfo[] = [
    { name: '渔具集市', icon: '🎣' },
    { name: '鱼竿专区', icon: '竿' },
    { name: '鱼饵窝料', icon: '🪱' },
    { name: '钓箱配件', icon: '🧰' },
    { name: '钓场预约', icon: '🏞️' },
    { name: '消息', icon: '✉️' },
    { name: '我的', icon: '👤' }
  ]
  private types: string[] = ['手竿', '路亚竿', '海竿', '矶竿', '前打竿', '冰钓竿']
  private waters: string[] = ['江河野钓', '湖库守钓', '黑坑塘钓', '溪流飞钓']
  private dayChips: string[] = ['3天', '7天', '15天', '30天']
  private pools: string[] = ['青龙湖', '月湖夜钓场', '千岛湖', '金沙湾水库']
  private slots: string[] = ['清晨 5:00', '上午 8:00', '下午 14:00', '夜钓 19:00']

  private items: GoodsItem[] = [
    { name: '化氏一味EX手竿5.4米', price: 469, desc: '碳布吨位高 轻硬挺', score: 96, num: 5, tag: '鲫鲤通杀', hot: true },
    { name: '达瓦黑鲈路亚竿M调', price: 799, desc: '枪柄直柄可选 远投顺滑', score: 95, num: 4, tag: '路亚党', hot: true },
    { name: '光威无法一本4.5米', price: 199, desc: '新手入门 皮实耐造', score: 93, num: 6, tag: '性价比', hot: false },
    { name: '开沃精工武乾坤', price: 1280, desc: '竞技级调性 回鱼快', score: 97, num: 4, tag: '竞技款', hot: false },
    { name: '渔之源矶竿2号3.6米', price: 289, desc: '导环顺滑 绕线不炸', score: 92, num: 5, tag: '矶钓', hot: false },
    { name: '小红帽路亚轮1000型', price: 359, desc: '浅线杯 轻量化机身', score: 94, num: 3, tag: '微物轮', hot: true },
    { name: '狼王纺车轮3000型', price: 219, desc: '8+1轴承 顺滑耐用', score: 91, num: 6, tag: '通用轮', hot: false },
    { name: '达金溪流飞钓竿', price: 688, desc: '软调优雅 保护子线', score: 92, num: 2, tag: '飞钓', hot: false },
    { name: '阿卢浮漂L05枣核型', price: 128, desc: '芦苇手工 吃口清晰', score: 96, num: 9, tag: '口诀神器', hot: true },
    { name: '黄金眼夜光漂套装', price: 158, desc: '高亮电池 夜钓必备', score: 93, num: 8, tag: '夜钓', hot: false },
    { name: '天元窝料五谷杂粮', price: 45, desc: '发酵工艺 打窝留鱼', score: 94, num: 30, tag: '囤货装', hot: false },
    { name: '老坛玉米粒诱饵', price: 32, desc: '酸香发酵 草鲤最爱', score: 95, num: 40, tag: '热销', hot: true }
  ]

配置数据包括底部导航标签数组(使用 Emoji 和文字混合作为图标,其中"鱼竿专区"使用汉字"竿"作为图标,体现了垂钓行业的特色)、渔具类型数组(手竿、路亚竿、海竿、矶竿、前打竿、冰钓竿)、适用水域数组(江河野钓、湖库守钓、黑坑塘钓、溪流飞钓)、租期套餐数组(3天、7天、15天、30天)、钓场名称数组和到钓时段数组(清晨、上午、下午、夜钓)。通用商品集合包含二十四款渔具产品,涵盖了手竿、路亚竿、矶竿、渔轮、浮漂、窝料、饵料、路亚饵、钓箱、支架、抄网、鱼护、防晒伞、钓椅和电子秤等多种品类,价格从 25 元的失手绳到 1280 元的竞技级鱼竿,覆盖了不同消费层次的用户。每个商品都标注了好评度(89-97 分区间)和营销标签,热门商品(hot: true)在列表中显示红色脉动的"热卖"标签。

四、鱼竿与鱼饵数据集合

在这里插入图片描述

鱼竿专区和鱼饵窝料标签页各自拥有独立的数据集合,展示了垂钓行业产品细分参数。

  private rods: GoodsItem[] = [
    { name: '一味EX 3.9米', price: 359, desc: '28调 混养快速回鱼', score: 96, num: 4, tag: '硬度4H', hot: true },
    { name: '一味EX 4.5米', price: 399, desc: '长身控鱼 护线不炸', score: 96, num: 5, tag: '硬度5H', hot: false },
    { name: '一味EX 5.4米', price: 469, desc: '远投打底 大物首选', score: 97, num: 5, tag: '硬度6H', hot: true },
    { name: '无法一本 3.6米', price: 159, desc: '入门结实 练竿利器', score: 93, num: 4, tag: '硬度3H', hot: false },
    { name: '无法一本 4.5米', price: 199, desc: '鲫鲤兼顾 皮实', score: 93, num: 4, tag: '硬度4H', hot: false },
    { name: '武乾坤 5.7米', price: 1280, desc: '竞技设计 飞磕神器', score: 97, num: 6, tag: '硬度8H', hot: false },
    { name: '竹山五代 4.5米', price: 139, desc: '桐木手作 复古情怀', score: 90, num: 3, tag: '硬度2H', hot: false },
    { name: '浪尖黑坑 4.8米', price: 520, desc: '黑坑专用 回鱼暴力', score: 95, num: 6, tag: '硬度7H', hot: true },
    { name: '斩鲈M调路亚竿', price: 439, desc: '泛用M调 软饵硬饵通吃', score: 94, num: 2, tag: '硬度M', hot: false },
    { name: '斩鲈H调路亚竿', price: 489, desc: '重障区 flipping首选', score: 94, num: 2, tag: '硬度H', hot: false },
    { name: '冰钓竿1.8米', price: 119, desc: '短节便携 冬钓神器', score: 91, num: 3, tag: '硬度2H', hot: false },
    { name: '前打竿7米', price: 699, desc: '滑漂远投 深水探底', score: 93, num: 2, tag: '硬度5H', hot: false }
  ]
  private baits: GoodsItem[] = [
    { name: '老坛酸香玉米', price: 32, desc: '发酵老坛 窝留双效', score: 95, num: 6, tag: '打窝', hot: true },
    { name: '五谷杂粮窝料', price: 45, desc: '续窝神器 不死窝', score: 94, num: 5, tag: '打窝', hot: false },
    { name: '红薯大鲤饵', price: 38, desc: '薯香雾化 诱钓合一', score: 93, num: 4, tag: '钓饵', hot: false },
    { name: '腥香鲫鱼饵', price: 35, desc: '冬鲫利器 状态拉糊', score: 92, num: 4, tag: '钓饵', hot: false },
    { name: '红虫拉饵伴侣', price: 26, desc: '拉红虫不脏手', score: 90, num: 3, tag: '辅助', hot: false },
    { name: '麝香酒米窝料', price: 29, desc: '酒米留鱼 聚鱼快', score: 91, num: 5, tag: '打窝', hot: false },
    { name: '果酸诱食剂', price: 19, desc: '夏季肥水 破口神助', score: 89, num: 3, tag: '小药', hot: false },
    { name: '雾化颗粒1.0', price: 22, desc: '散炮伴侣 层层雾化', score: 90, num: 4, tag: '散炮', hot: false },
    { name: '虾滑拉饵', price: 33, desc: '虾肉拉饵 罗非最爱', score: 91, num: 3, tag: '钓饵', hot: false },
    { name: '麸香底窝料', price: 28, desc: '沉底留香 久钓不散', score: 90, num: 4, tag: '打窝', hot: false }
  ]

鱼竿数据包含十二款产品,tag 字段格式为"硬度NH"(2H-8H)或"硬度M/H"(路亚竿调性),num 字段对应硬度数字(2-8)。硬度越高代表鱼竿越硬,适合钓大鱼和快速回鱼。这些硬度参数在后续的硬度参数柱状图中将用于可视化对比。鱼饵数据包含十款产品,分为打窝料(老坛玉米、五谷杂粮、麝香酒米、麸香底窝)、钓饵(红薯大鲤饵、腥香鲫鱼饵、虾滑拉饵)、辅助料(红虫拉饵伴侣)和小药添加剂(果酸诱食剂、雾化颗粒)等类型,tag 字段标识用途分类。热门鱼饵(hot: true)在列表中显示红色脉动的"热销"标签。这些数据充分体现了垂钓行业从鱼竿调性到饵料配方的专业细分。

五、钓箱配件与钓场数据集合

在这里插入图片描述

钓箱配件和钓场预约标签页拥有各自的数据集合,展示了配套装备和钓场信息的完整数据。

  private boxes: GoodsItem[] = [
    { name: '竞技钓箱28L', price: 268, desc: 'EPP保温 四脚升降', score: 94, num: 28, tag: '钓箱', hot: true },
    { name: '便携钓箱18L', price: 168, desc: '轻量短途 手提舒适', score: 92, num: 18, tag: '钓箱', hot: false },
    { name: '伞架炮台套装', price: 129, desc: '万能接口 稳如泰山', score: 92, num: 3, tag: '支架', hot: false },
    { name: '夜钓灯蓝光充电款', price: 99, desc: '不惊鱼 聚光省电', score: 93, num: 12, tag: '夜钓', hot: true },
    { name: '多功能子线盒', price: 39, desc: '防锈绕线 双面收纳', score: 90, num: 8, tag: '收纳', hot: false },
    { name: '饵料开饵盆', price: 29, desc: '量杯刻度 一盆搞定', score: 89, num: 6, tag: '开饵', hot: false },
    { name: '鱼钩绑线器', price: 25, desc: '绑钩神器 新手友好', score: 91, num: 5, tag: '工具', hot: false },
    { name: '防晒钓鱼伞2.4米', price: 158, desc: '银胶防晒 双层抗风', score: 93, num: 4, tag: '遮阳', hot: false },
    { name: '防水饵料包', price: 59, desc: '分层收纳 防潮密封', score: 90, num: 7, tag: '收纳', hot: false },
    { name: '控鱼器不锈钢', price: 45, desc: '安全摘钩 不伤手', score: 91, num: 6, tag: '摘钩', hot: false }
  ]
  private pools2: GoodsItem[] = [
    { name: '青龙湖野钓区', price: 80, desc: '3号位鲫鱼连竿 圣地', score: 95, num: 3, tag: '野钓', hot: true },
    { name: '月湖夜钓场', price: 100, desc: '灯光夜钓 大物出没', score: 94, num: 5, tag: '夜钓', hot: true },
    { name: '千岛湖库湾', price: 150, desc: '湖库巨物 需提前约', score: 96, num: 12, tag: '湖库', hot: false },
    { name: '金沙湾水库', price: 120, desc: '休闲大库 环境清幽', score: 93, num: 8, tag: '湖库', hot: false },
    { name: '城市黑坑一号', price: 200, desc: '高密度塘 正钓爽', score: 92, num: 2, tag: '黑坑', hot: false },
    { name: '城市黑坑二号', price: 180, desc: '偷驴好去处 鱼口滑', score: 90, num: 2, tag: '黑坑', hot: false },
    { name: '溪流飞钓点', price: 60, desc: '山涧冷水鱼 清爽', score: 91, num: 1, tag: '溪流', hot: false },
    { name: '江湾筏钓平台', price: 130, desc: '筏钓翘嘴 深水区', score: 92, num: 6, tag: '筏钓', hot: false },
    { name: '亲子休闲渔场', price: 50, desc: '小孩免费 鱼好钓', score: 89, num: 1, tag: '亲子', hot: false },
    { name: '竞技练竿塘', price: 90, desc: '按小时计 回鱼快', score: 93, num: 3, tag: '练竿', hot: false }
  ]

钓箱配件数据包含十款产品,num 字段记录容量规格(28L、18L 的钓箱容量,或配件的通用数字),tag 字段标识产品分类(钓箱、支架、夜钓、收纳、开饵、工具、遮阳、摘钩)。钓场数据包含十个钓场,price 字段为钓位费用(50-200 元/位),num 字段记录水深(1-12 米),desc 字段描述钓场特色。钓场类型覆盖了野钓、夜钓、湖库、黑坑、溪流、筏钓、亲子和练竿等多种场景。热门钓场(hot: true)在列表中显示红色脉动的"爆满预警"标签,提示用户预约紧张。这些数据为钓场预约弹窗中的选择器提供了选项来源,也为水深柱状图和渔情指数卡片提供了数据基础。

六、工具方法与安全访问封装

在这里插入图片描述

应用封装了一系列工具方法来处理数据查询、状态读取和统计计算。

  selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }
  selDesc(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.desc
  }
  selTag(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.tag
  }
  selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }
  selScore(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.score
  }
  maxOfNum(list: GoodsItem[]): number {
    let m: number = 0
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }
  unreadCount(): number {
    let c: number = 0
    for (let i = 0; i < this.msgs.length; i++) {
      if (!this.msgs[i].read) {
        c++
      }
    }
    return c
  }

selNameselDescselTagselPriceselScore 这组方法实现了对 selectedItem 的安全访问封装,每个方法首先检查是否为 null,空值时返回默认值(空字符串或 0)。这些方法在详情弹窗中被调用以展示选中商品的信息。maxOfNum 是通用方法,接收 GoodsItem 数组参数,返回数组中 num 字段的最大值,被鱼竿硬度柱状图、鱼饵库存柱状图、钓箱容量柱状图和钓场水深柱状图共用,体现了代码复用的设计原则。unreadCount 方法遍历 msgs 数组统计 readfalse 的消息数量,返回值用于头部栏的消息角标和消息中心的未读数显示。这些方法虽然未加 private 修饰符,但实际只在组件内部使用,是 ArkTS 中封装内部逻辑的常见做法。

七、头部栏构建器

头部栏集成了地理位置、品牌标识、爆护榜入口、消息角标和实时渔情信息,是应用信息密度最高的区域。

  @Builder buildHeader() {
    Column() {
      Row() {
        Column() {
          Row() {
            Text('📍').fontSize(12)
            Text('杭州 · 西湖区').fontSize(12).fontColor(Color.White).margin({ left: 4 })
            Text('›').fontSize(12).fontColor('rgba(255,255,255,0.7)').margin({ left: 4 })
          }
          .alignItems(VerticalAlign.Center)

          Text('京东垂钓渔具馆').fontSize(19).fontColor(Color.White).fontWeight(FontWeight.Bold).margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('🎣').fontSize(20)
          Text('爆护榜').fontSize(9).fontColor('rgba(255,255,255,0.8)').margin({ top: 2 })
        }
        .width(52)
        .onClick(() => {
          this.currentTab = 0
        })

        Column() {
          Text('✉️').fontSize(20)
          if (this.unreadCount() > 0) {
            Text(this.unreadCount().toString())
              .fontSize(8)
              .fontColor(Color.White)
              .backgroundColor('#FF3B30')
              .borderRadius(7)
              .padding({ left: 4, right: 4 })
              .position({ x: 14, y: -6 })
              .scale({ x: 1.15, y: 1.15 })
              .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
          }
        }
        .width(52)
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('渔情:今日 18-24℃ 微风').fontSize(11).fontColor('#E3F6ED')
          Text('鲫鱼开口期 · 浅滩 1.5-2.5 米').fontSize(10).fontColor('rgba(255,255,255,0.75)').margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('今日宜钓').fontSize(11).fontColor(Color.White).backgroundColor('#558B2F').borderRadius(10).padding({ left: 8, right: 8, top: 4, bottom: 4 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor('#0277BD')
  }

头部栏分为上下两行,是所有标签页中信息最丰富的头部设计。第一行左侧是地理位置定位(📍图标 + “杭州 · 西湖区” + 箭头符号)和品牌名称"京东垂钓渔具馆",使用白色粗体字。中间是"爆护榜"入口(🎣图标 + 文字),点击跳转到渔具集市标签页。右侧是消息入口,使用 ✉️ 图标,当 unreadCount() 大于 0 时显示红色未读数角标,角标使用 position 绝对定位偏移到图标右上角并添加了 1.15 倍缩放和 650 毫秒的脉动循环动画。第二行是渔情信息条,左侧展示温度和风力数据(“今日 18-24℃ 微风”)和鱼情判断(“鲫鱼开口期 · 浅滩 1.5-2.5 米”),右侧是"今日宜钓"绿色标签。整个头部栏背景色设为 #0277BD(深蓝色),呼应了水域和垂钓的主题色调。这种将地理位置、品牌、功能入口和实时渔情信息集成在头部栏的设计,充分体现了垂钓应用对场景化信息的重视。

八、渔具集市标签页

渔具集市是应用首页标签,展示运营数据统计、精选商品列表和租赁入口。

  @Builder buildTab1() {
    Column() {
      Row({ space: 8 }) {
        Column() {
          Text('🎣').fontSize(22)
          Text('爆护率').fontSize(10).fontColor('#5C6280').margin({ top: 4 })
          Text('78%').fontSize(15).fontColor('#0277BD').fontWeight(FontWeight.Bold).margin({ top: 2 })
        }
        .layoutWeight(1)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding({ top: 10, bottom: 10 })

        Column() {
          Text('🏆').fontSize(22)
          Text('本周鱼获').fontSize(10).fontColor('#5C6280').margin({ top: 4 })
          Text('12.6吨').fontSize(15).fontColor('#558B2F').fontWeight(FontWeight.Bold).margin({ top: 2 })
        }
        .layoutWeight(1)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding({ top: 10, bottom: 10 })

        Column() {
          Text('👥').fontSize(22)
          Text('今日出钓').fontSize(10).fontColor('#5C6280').margin({ top: 4 })
          Text('3269人').fontSize(15).fontColor('#D84315').fontWeight(FontWeight.Bold).margin({ top: 2 })
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 750, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding({ top: 10, bottom: 10 })
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 12 })

      Row() {
        Text('为你精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        Text('按销量排序'.slice(0, 6)).fontSize(10).fontColor('#9AA0B5').margin({ left: 8 })
        Text('筛选 ›').fontSize(10).fontColor('#0277BD').margin({ left: 12 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      ForEach(this.items, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text(g.tag.slice(0, 1)).fontSize(24).fontColor(Color.White)
          }
          .width(72)
          .height(72)
          .borderRadius(12)
          .backgroundColor(i % 3 === 0 ? '#0277BD' : (i % 3 === 1 ? '#558B2F' : '#0288D1'))
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Column() {
                Text(g.tag).fontSize(9).fontColor('#558B2F').backgroundColor('#E3F6ED').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
              }
              if (g.hot) {
                Column() {
                  Text('热卖').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
                }
                .margin({ left: 6 })
                .scale({ x: 1.08, y: 1.08 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .margin({ top: 5 })
            .alignItems(VerticalAlign.Center)

            Stack({ alignContent: Alignment.Start }) {
              Column().width('100%').height(6).backgroundColor('#F0F0F5').borderRadius(3)
              Column().width(g.score + '%').height(6).backgroundColor('#0277BD').borderRadius(3)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Text('¥' + g.price).fontSize(16).fontColor('#D84315').fontWeight(FontWeight.Bold)
            Column() {
              Text('立即租赁').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#558B2F')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 8 })
            .onClick(() => {
              this.selectedItem = g
              this.rentDays = 3
              this.showRent = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.selectedItem = g
          this.showDetail = true
        })
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

渔具集市标签页顶部是三列数据统计卡片,使用 Row({ space: 8 }) 创建带间距的水平排列。三列分别展示爆护率(78%,蓝色)、本周鱼获(12.6吨,绿色)和今日出钓人数(3269人,橙红色)。出钓人数添加了缩放脉动动画。下方是"为你精选"标题行,包含排序和筛选入口。商品列表通过 ForEach 遍历 items 数组,每项为白色圆角卡片。卡片左侧为标签首字符的彩色方块(使用 i % 3 循环切换深蓝、绿色、浅蓝三种背景色),中间为商品信息区(包含名称、描述、标签胶囊和"热卖"标签、好评度进度条),右侧为价格和"立即租赁"按钮。好评度进度条使用 Stack 叠加方式,宽度为 g.score + '%'。热门商品的"热卖"标签使用红色背景并添加脉动动画。点击"立即租赁"按钮设置 selectedItem、初始化 rentDays 为 3,并触发租赁弹窗。点击卡片整体触发详情弹窗。

九、鱼竿专区标签页

鱼竿专区以"硬度矩阵"为核心概念,通过 Flex 双列布局和硬度参数柱状图展示鱼竿产品。

  @Builder buildTab2() {
    Column() {
      Text('鱼竿硬度矩阵').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.rods, (g: GoodsItem, i: number) => {
          Column() {
            Row() {
              Column() {
                Text('🎣').fontSize(20)
              }
              .width(44)
              .height(44)
              .borderRadius(22)
              .backgroundColor(i % 2 === 0 ? '#E3F2FA' : '#E3F6ED')
              .justifyContent(FlexAlign.Center)

              Column() {
                Text(g.tag).fontSize(10).fontColor('#0277BD').fontWeight(FontWeight.Bold)
                Text('好评 ' + g.score + '%').fontSize(9).fontColor('#8A8FA3').margin({ top: 3 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Text(g.name).fontSize(12).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
            Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text('¥' + g.price).fontSize(14).fontColor('#D84315').fontWeight(FontWeight.Bold)
              if (g.hot) {
                Text('🔥').fontSize(11).margin({ left: 6 })
                  .scale({ x: 1.15, y: 1.15 })
                  .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .width('100%')
            .margin({ top: 6 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .width('48%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

      Column() {
        Text('硬度参数柱状图').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Row({ space: 10 }) {
          ForEach(this.rods, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.rods) * 90)
                .backgroundColor('#8BD4A8')
                .borderRadius(4)
              Text(g.tag.slice(2, 4)).fontSize(8).fontColor('rgba(255,255,255,0.85)').margin({ top: 4 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
        .alignItems(VerticalAlign.End)
      }
      .width('100%')
      .backgroundColor('#2E5E3A')
      .borderRadius(14)
      .padding(14)
      .margin({ left: 12, right: 12, top: 4 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

鱼竿专区分为上下两个区域。上方是"鱼竿硬度矩阵"标题和 Flex 双列布局的产品列表。每个产品卡片宽度为 48%,内部包含圆形鱼竿图标(使用 i % 2 切换两种浅色背景)、硬度标签和好评率(右上角对齐)、产品名称和描述、价格行。热门产品的价格旁显示 🔥 图标并添加 1.15 倍缩放脉动动画。点击卡片触发详情弹窗。下方是"硬度参数柱状图",使用深绿色背景卡片。柱状图通过 ForEach 遍历 rods 数组,柱子高度按 g.num / this.maxOfNum(this.rods) * 90 计算(最大值为 8,即武乾坤的 8H 硬度),颜色为浅绿色(#8BD4A8),底部标注从 g.tag 中截取的硬度数字(slice(2, 4) 提取"4H"、"5H"等)。这个柱状图直观展示了不同鱼竿的硬度对比,帮助用户根据目标鱼种选择合适调性的鱼竿。

十、鱼饵窝料标签页

鱼饵窝料标签页通过排名列表和横向滚动速览两种布局展示饵料产品。

  @Builder buildTab3() {
    Column() {
      Text('鱼饵窝料 · 库存排行').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        ForEach(this.baits, (g: GoodsItem, i: number) => {
          Row() {
            Text((i + 1).toString()).fontSize(12).fontColor(Color.White).backgroundColor(i < 3 ? '#D84315' : '#9AA0B5').borderRadius(10).width(20).height(20).textAlign(TextAlign.Center)

            Column()
              .width(4)
              .height(44)
              .backgroundColor(i % 3 === 0 ? '#558B2F' : (i % 3 === 1 ? '#0277BD' : '#D84315'))
              .borderRadius(2)
              .margin({ left: 10 })

            Column() {
              Row() {
                Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
                Text(g.tag).fontSize(9).fontColor('#558B2F').backgroundColor('#E3F6ED').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
              }
              .alignItems(VerticalAlign.Center)

              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(7).backgroundColor('#F0F0F5').borderRadius(4)
                Column().width(g.num / this.maxOfNum(this.baits) * 100 + '%').height(7).backgroundColor('#0277BD').borderRadius(4)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 10 })

            Column() {
              Text('¥' + g.price).fontSize(15).fontColor('#D84315').fontWeight(FontWeight.Bold)
              Text('剩 ' + g.num * 5 + ' 件').fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

鱼饵窝料标签页顶部是"库存排行"标题,下方是排名列表。每项使用排名序号(前三名橙红色背景,其余灰色)、彩色竖条标识(i % 3 循环切换绿色、蓝色、橙红色三种颜色)、产品信息(名称和用途标签)和库存进度条。库存进度条宽度按 g.num / this.maxOfNum(this.baits) * 100 + '%' 计算,直观展示各饵料的库存对比。右侧为价格和剩余件数(g.num * 5 件,将包数转换为件数展示)。点击列表项触发详情弹窗。排名列表的设计让用户快速了解饵料的库存排行和热度,前三名的橙红色序号突出了最受欢迎的产品。

列表下方是"小药添加剂 · 横向速览"区域,使用 Scroll 组件配合 ScrollDirection.Horizontal 实现横向滚动列表。每个饵料卡片宽度为 104 像素,包含 🪱 图标、名称、价格和好评率。隐藏滚动条保持界面整洁。横向滚动区为小药和添加剂类产品提供了快速浏览的入口,与上方的排名列表形成了主次分明的布局结构。

十一、钓箱配件标签页

钓箱配件标签页通过统计卡片和双列精选布局展示配套装备。

  @Builder buildTab4() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('钓箱配件仓').fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Bold)
            Text('钓箱 · 支架 · 夜钓灯 · 收纳').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('🧰').fontSize(30)
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row({ space: 8 }) {
          ForEach(this.boxes, (g: GoodsItem) => {
            Column() {
              Column()
                .width(14)
                .height(g.num / this.maxOfNum(this.boxes) * 70)
                .backgroundColor('#4FC3F7')
                .borderRadius(3)
              Text(g.tag).fontSize(8).fontColor('rgba(255,255,255,0.8)').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 14 })
        .alignItems(VerticalAlign.End)

        Text('容量/规格柱状图(升 / 厘米)').fontSize(9).fontColor('rgba(255,255,255,0.55)').margin({ top: 8 })
      }
      .width('100%')
      .backgroundColor('#01579B')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('装备双列精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.boxes, (g: GoodsItem, i: number) => {
          Column() {
            Column() {
              Text('🧰').fontSize(26)
            }
            .width('100%')
            .height(70)
            .borderRadius(10)
            .backgroundColor(i % 2 === 0 ? '#E3F2FA' : '#EFEBE0')
            .justifyContent(FlexAlign.Center)

            Text(g.name).fontSize(12).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
            Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text('¥' + g.price).fontSize(14).fontColor('#D84315').fontWeight(FontWeight.Bold)
              Text('租').fontSize(10).fontColor(Color.White).backgroundColor('#558B2F').borderRadius(4).padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 6 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .width('48%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

钓箱配件标签页顶部是深蓝色统计卡片,左侧展示"钓箱配件仓"标题和副文案,右侧为 🧰 图标并添加了缩放脉动动画。卡片内嵌一个容量/规格柱状图,柱子高度按 g.num / this.maxOfNum(this.boxes) * 70 计算(最大值为 28,即 28L 竞技钓箱),颜色为浅蓝色(#4FC3F7),底部标注产品分类标签。下方是"装备双列精选" Flex 布局,每个卡片宽度为 48%,包含装备图标(使用 i % 2 切换两种背景色)、名称、描述和价格行。价格行右侧的"租"标签使用绿色背景,表明该装备支持租赁。点击卡片触发详情弹窗。整体设计通过统计卡片提供数据概览,通过双列布局展示详细产品,形成了从宏观到微观的信息层次。

十二、钓场预约标签页

钓场预约标签页是应用最具垂钓特色的模块,展示渔情指数、水深柱状图和钓场列表。

  @Builder buildTab5() {
    Column() {
      Column() {
        Text('本周渔情指数').fontSize(14).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Row() {
          Column() {
            Text('78').fontSize(26).fontColor('#8BD4A8').fontWeight(FontWeight.Bold)
            Text('综合渔情').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('3.2万').fontSize(26).fontColor('#FFF59D').fontWeight(FontWeight.Bold).margin({ left: 24 })
              .scale({ x: 1.04, y: 1.04 })
              .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            Text('本周出钓人次').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2, left: 24 })
          }
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('46斤').fontSize(26).fontColor('#FFAB91').fontWeight(FontWeight.Bold).margin({ left: 24 })
            Text('单尾最重记录').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2, left: 24 })
          }
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .margin({ top: 12 })
        .alignItems(VerticalAlign.Top)
      }
      .width('100%')
      .backgroundColor('#558B2F')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('钓场水深柱状图(米)').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Bold)
        Row({ space: 12 }) {
          ForEach(this.pools2, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.pools2) * 100)
                .backgroundColor('#0277BD')
                .borderRadius(4)
              Text(g.name.slice(0, 2)).fontSize(8).fontColor('#8A8FA3').margin({ top: 4 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
        .alignItems(VerticalAlign.End)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('全部钓场').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      ForEach(this.pools2, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text('🏞️').fontSize(24)
          }
          .width(56)
          .height(56)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#E3F2FA' : '#E3F6ED')
          .justifyContent(FlexAlign.Center)

          Column() {
            Row() {
              Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
              Text(g.tag).fontSize(9).fontColor('#0277BD').backgroundColor('#E3F2FA').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
              if (g.hot) {
                Text('爆满预警').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .alignItems(VerticalAlign.Center)

            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            Text('水深 ' + g.num + ' 米 · 好评 ' + g.score + '%').fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Text('¥' + g.price + '/位').fontSize(14).fontColor('#D84315').fontWeight(FontWeight.Bold)
            Column() {
              Text('预约钓位').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#0277BD')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 6 })
            .onClick(() => {
              this.selectedItem = g
              this.personNum = 2
              this.showPool = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

钓场预约标签页分为三个区域。顶部是绿色"本周渔情指数"卡片,展示三个核心数据:综合渔情指数(78,浅绿色)、本周出钓人次(3.2万,黄色,添加脉动动画)和单尾最重记录(46斤,橙色)。这三个数据从不同维度反映了本周的垂钓热度。中间是"钓场水深柱状图",柱子高度按 g.num / this.maxOfNum(this.pools2) * 100 计算(最大值为 12,即千岛湖库湾的 12 米水深),颜色为深蓝色,底部标注钓场名前两字。这个柱状图帮助用户了解各钓场的水深情况,以便选择适合目标鱼种的钓场。下方是"全部钓场"列表,通过 ForEach 遍历 pools2 数组生成钓场卡片。每项包含钓场图标、名称、类型标签、描述、水深和好评率信息。热门钓场显示红色脉动的"爆满预警"标签。右侧为钓位价格(¥/位格式)和"预约钓位"按钮,点击设置 selectedItem、初始化 personNum 为 2 并触发钓位预约弹窗。

十三、消息中心与用户中心标签页

消息中心展示通知列表并支持删除操作,用户中心展示个人资料、数据统计和功能菜单。

  @Builder buildTab6() {
    Column() {
      Row() {
        Text('消息中心').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
        Text('未读 ' + this.unreadCount() + ' 条').fontSize(11).fontColor('#FF3B30').margin({ left: 8 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 10 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      ForEach(this.msgs, (m: MsgItem, i: number) => {
        Row() {
          Column() {
            Text(m.read ? '📭' : '🐟').fontSize(20)
            if (!m.read) {
              Column()
                .width(8)
                .height(8)
                .borderRadius(4)
                .backgroundColor('#FF3B30')
                .position({ x: 30, y: -2 })
                .scale({ x: 1.2, y: 1.2 })
                .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
            }
          }
          .width(44)

          Column() {
            Row() {
              Text(m.title).fontSize(13).fontColor(m.read ? '#5C6280' : '#212121').fontWeight(m.read ? FontWeight.Normal : FontWeight.Bold).maxLines(1)
              Text(m.time).fontSize(9).fontColor('#9AA0B5').margin({ left: 6 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)

            Text(m.content).fontSize(11).fontColor('#8A8FA3').margin({ top: 4 }).maxLines(2)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 8 })

          Column() {
            Text('删除').fontSize(10).fontColor('#9AA0B5')
          }
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .border({ width: 1, color: '#E5E5EC' })
          .borderRadius(10)
          .onClick(() => {
            this.selectedItem = null
            this.deleteIndex = i
            this.showDelete = true
          })
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
      }, (m: MsgItem) => m.title + m.time)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

消息中心标题行展示"消息中心"和"未读 N 条",未读数使用红色字体并添加 1.05 倍缩放脉动动画。消息列表通过 ForEach 遍历 msgs 数组,每项左侧为图标(已读用信箱 Emoji 📭、未读用鱼 Emoji 🐟,体现了垂钓特色)和未读红点标识。中间为标题行和时间戳,标题的字体颜色和粗细根据已读状态区分:未读为深色加粗,已读为浅色正常字重。内容预览限制为两行。右侧"删除"按钮使用边框样式,点击设置 deleteIndex 为当前索引 i 并触发删除确认弹窗。ForEach 的键值生成器使用 m.title + m.time 作为唯一键。deleteIndex 状态变量在删除确认弹窗中通过 splice 方法从 msgs 数组中移除对应项。

用户中心标签页展示了用户头像(🎣 图标)、昵称(“静水_守钓人”)、等级标签(“黄金钓手”)和爆护次数(“爆护 12 次”),以及租赁中、我的预约、收藏渔具和优惠券四个统计数字卡片。功能菜单包含我的订单、我的爆护记录、渔具保养卡、常去钓场和设置等垂钓特色入口。最底部是"发布闲置渔具"按钮,点击后初始化 selTypeselWater 并触发发布弹窗。

十四、底部导航栏构建器

底部导航栏使用 Emoji 图标和文字标签组合,通过颜色区分当前选中标签。

  @Builder buildBottom() {
    Row() {
      ForEach(this.tabs, (t: TabInfo, i: number) => {
        Column() {
          Text(t.icon).fontSize(18)
          Text(t.name).fontSize(9).fontColor(this.currentTab === i ? '#0277BD' : '#9AA0B5').margin({ top: 2 })
        }
        .layoutWeight(1)
        .padding({ top: 6, bottom: 6 })
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = i
        })
      }, (t: TabInfo) => t.name)
    }
    .width('100%')
    .backgroundColor(Color.White)
    .border({ width: 1, color: '#EDEDF2' })
  }

底部导航栏使用 ForEach 遍历 tabs 数组生成七个导航项。每个导航项为 Column,内部包含图标(Emoji 或文字)和文字标签。当前选中标签的文字颜色为深蓝色(#0277BD),未选中为灰色(#9AA0B5)。每个 Column 通过 layoutWeight(1) 均分水平空间,点击更新 currentTab。底部栏白色背景配合上边框线。值得注意的是,"鱼竿专区"标签的图标使用汉字"竿"而非 Emoji,这是垂钓行业应用的一个特色设计细节。

十五、发布闲置弹窗

发布闲置弹窗以居中模态方式显示,提供渔具名称输入、类型选择和水域选择功能。

  @Builder publishModal() {
    Column() {
      Column() {
        Row() {
          Text('发布闲置渔具').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('渔具名称').fontSize(12).fontColor('#5C6280')
          TextInput({ placeholder: '如:化氏一味EX 5.4米 用过一季' })
            .height(40)
            .backgroundColor('#F5F6FA')
            .borderRadius(8)
            .margin({ top: 6 })
            .fontSize(12)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 16 })

        Column() {
          Text('渔具类型').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.types, (k: string, i: number) => {
              Text(k).fontSize(11).fontColor(this.selType === i ? Color.White : '#5C6280')
                .backgroundColor(this.selType === i ? '#0277BD' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selType = i
                })
            }, (k: string) => k)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('适用水域').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.waters, (w: string, i: number) => {
              Text(w).fontSize(11).fontColor(this.selWater === i ? Color.White : '#5C6280')
                .backgroundColor(this.selWater === i ? '#558B2F' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selWater = i
                })
            }, (w: string) => w)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Column() {
            Text('暂不发布').fontSize(13).fontColor('#5C6280')
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#F5F6FA')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showPublish = false
          })

          Column() {
            Text('确认发布').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1.6)
          .height(40)
          .backgroundColor('#0277BD')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .scale({ x: 1.02, y: 1.02 })
          .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.showPublish = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .width('92%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showPublish = false
    })
  }

发布弹窗以居中模态方式显示,宽度为 92%。内容包括标题行、渔具名称输入框(TextInput 组件)、渔具类型选择器(手竿、路亚竿、海竿、矶竿、前打竿、冰钓竿,选中态深蓝色)和适用水域选择器(江河野钓、湖库守钓、黑坑塘钓、溪流飞钓,选中态绿色)。两个选择器都使用 Flex 配合 FlexWrap.Wrap 实现自动换行。底部"暂不发布"和"确认发布"按钮分别使用浅灰色和深蓝色背景,"确认发布"按钮占更大宽度(layoutWeight(1.6))并添加了缩放脉动动画。constraintSize({ maxHeight: '80%' }) 限制弹窗最大高度,外部半透明遮罩点击关闭弹窗,内部 stopPropagation 阻止冒泡。渔具类型和水域选择器的双色区分(蓝色和绿色)分别对应了渔具属性和环境属性,在视觉上形成了清晰的分类。

十六、渔具租赁弹窗

渔具租赁弹窗从底部弹出,提供租期套餐选择、押金计算和天数调整功能。

  @Builder rentModal() {
    Column() {
      Column() {
        Row() {
          Text('渔具租赁').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text(this.selName()).fontSize(14).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
          Text(this.selDesc()).fontSize(11).fontColor('#8A8FA3').margin({ top: 4 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('租期套餐').fontSize(12).fontColor('#5C6280')
          Row({ space: 8 }) {
            ForEach(this.dayChips, (t: string, i: number) => {
              Column() {
                Text(t).fontSize(12).fontColor(this.selDay === i ? '#558B2F' : '#5C6280')
                if (i === 1) {
                  Text('赠窝料').fontSize(8).fontColor(Color.White).backgroundColor('#D84315').borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 }).margin({ top: 3 })
                    .scale({ x: 1.1, y: 1.1 })
                    .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
                }
              }
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .backgroundColor(this.selDay === i ? '#E3F6ED' : '#F5F6FA')
              .borderRadius(10)
              .onClick(() => {
                this.selDay = i
              })
            }, (t: string) => t)
          }
          .margin({ top: 8 })
          .alignItems(VerticalAlign.Top)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Column() {
            Text('押金(可退)').fontSize(11).fontColor('#5C6280')
            Text('¥' + (this.rentDays * 20)).fontSize(22).fontColor('#D84315').fontWeight(FontWeight.Bold).margin({ top: 4 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Column() {
            Text('调整天数').fontSize(11).fontColor('#5C6280')
            Row() {
              Text('-').fontSize(16).fontColor('#0277BD').width(30).height(30).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.rentDays > 1) {
                    this.rentDays--
                  }
                })
              Text(this.rentDays.toString() + ' 天').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).width(64).textAlign(TextAlign.Center)
              Text('+').fontSize(16).fontColor('#0277BD').width(30).height(30).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.rentDays < 60) {
                    this.rentDays++
                  }
                })
            }
            .margin({ top: 6 })
            .alignItems(VerticalAlign.Center)
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .margin({ top: 18 })
        .alignItems(VerticalAlign.Top)

        Row() {
          Column() {
            Text('到店自取').fontSize(11).fontColor('#8A8FA3')
          }
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .border({ width: 1, color: '#E5E5EC' })
          .borderRadius(12)
          .onClick(() => {
            this.showRent = false
          })

          Column() {
            Text('免押金 · 立即租').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#558B2F')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showRent = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End)
    .onClick(() => {
      this.showRent = false
    })
  }

租赁弹窗使用 justifyContent(FlexAlign.End) 从底部弹出。弹窗顶部通过 selName()selDesc() 安全获取商品信息。租期套餐选择器展示四个选项(3天、7天、15天、30天),第二个选项(7天)额外显示"赠窝料"红色标签并添加脉动动画,这是垂钓行业的特色营销手段。押金计算公式为 this.rentDays * 20(每天 20 元押金),押金金额添加了缩放脉动动画。天数调节器使用减号和加号的 Text 组件,减号限制最小 1 天,加号限制最大 60 天(垂钓场景的租赁周期通常较短)。底部提供"到店自取"(边框样式)和"免押金 · 立即租"(绿色填充样式)两个操作按钮。租期套餐选择器的选中态使用绿色背景(#E3F6ED),与押金按钮的绿色形成统一的色调体系,呼应了垂钓的户外自然主题。

十七、钓位预约弹窗

钓位预约弹窗是应用最具特色的弹窗,提供钓场选择、时段选择和同行人数调整功能。

  @Builder poolModal() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('预约钓位').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
            Text('在线选位 · 到场扫码开钓').fontSize(10).fontColor('#558B2F').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)

          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('选择钓场').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.pools, (p: string, i: number) => {
              Text(p).fontSize(11).fontColor(this.selPool === i ? Color.White : '#5C6280')
                .backgroundColor(this.selPool === i ? '#0277BD' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selPool = i
                })
            }, (p: string) => p)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 16 })

        Column() {
          Text('到钓时段').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.slots, (s: string, i: number) => {
              Text(s).fontSize(11).fontColor(this.selSlot === i ? Color.White : '#5C6280')
                .backgroundColor(this.selSlot === i ? '#558B2F' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selSlot = i
                })
            }, (s: string) => s)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Text('同行人数').fontSize(12).fontColor('#5C6280')
          Row({ space: 10 }) {
            Text('-').fontSize(15).fontColor('#0277BD').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
              .onClick(() => {
                if (this.personNum > 1) {
                  this.personNum--
                }
              })
            Text(this.personNum.toString() + ' 人').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium)
            Text('+').fontSize(15).fontColor('#0277BD').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
              .onClick(() => {
                if (this.personNum < 12) {
                  this.personNum++
                }
              })
          }
          .alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        .margin({ top: 16 })

        Column() {
          Text('预计费用 ¥' + (this.personNum * 80)).fontSize(16).fontColor('#D84315').fontWeight(FontWeight.Bold)
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('含钓位费与基础窝料一份').fontSize(10).fontColor('#9AA0B5').margin({ top: 4 })
        }
        .width('100%')
        .margin({ top: 14 })

        Column() {
          Text('锁定钓位').fontSize(14).fontColor(Color.White).fontWeight(FontWeight.Medium)
        }
        .width('100%')
        .height(42)
        .backgroundColor('#0277BD')
        .borderRadius(21)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 14 })
        .onClick(() => {
          this.showPool = false
        })
      }
      .width('92%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showPool = false
    })
  }

钓位预约弹窗以居中模态方式显示,标题旁标注"在线选位 · 到场扫码开钓"的特色说明。弹窗包含三个选择维度:钓场选择器(青龙湖、月湖夜钓场、千岛湖、金沙湾水库,选中态深蓝色)、到钓时段选择器(清晨 5:00、上午 8:00、下午 14:00、夜钓 19:00,选中态绿色)和同行人数调节器(限制 1-12 人)。费用计算公式为 this.personNum * 80(每人 80 元),费用金额添加了缩放脉动动画,并附注"含钓位费与基础窝料一份"说明。底部"锁定钓位"按钮使用深蓝色填充背景。两个选择器的双色区分(蓝色和绿色)与发布弹窗的设计保持一致:蓝色代表钓场属性,绿色代表时段/环境属性。同行人数调节器的设计与租赁弹窗的天数调节器相同,但上限设为 12 人,符合垂钓群体出行的实际情况。

十八、删除确认与详情弹窗

删除确认弹窗是居中显示的对话框,详情弹窗从右侧滑入展示商品完整信息。

  @Builder deleteModal() {
    Column() {
      Column() {
        Text('🗑️').fontSize(38)
          .scale({ x: 1.12, y: 1.12 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        Text('确认删除这条消息?').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold).margin({ top: 12 })
        Text('删除后聊天记录将不可恢复').fontSize(11).fontColor('#9AA0B5').margin({ top: 6 })

        Row() {
          Column() {
            Text('再想想').fontSize(13).fontColor('#5C6280')
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#F5F6FA')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showDelete = false
          })

          Column() {
            Text('确认删除').fontSize(13).fontColor(Color.White)
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#D84315')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            if (this.deleteIndex >= 0 && this.deleteIndex < this.msgs.length) {
              this.msgs.splice(this.deleteIndex, 1)
            }
            this.deleteIndex = -1
            this.showDelete = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .width('76%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(20)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

删除确认弹窗居中显示,宽度为 76%。内容包含垃圾桶 Emoji 图标(1.12 倍缩放脉动动画)、确认文案、说明文案和两个按钮。"再想想"按钮使用浅灰色背景,"确认删除"按钮使用橙红色背景。点击"确认删除"后,代码检查 deleteIndex 有效性(>= 0< this.msgs.length),通过 splice 方法从 msgs 数组移除对应项,然后重置 deleteIndex 为 -1 并关闭弹窗。由于 msgs@State 变量,数组变化自动触发消息列表重新渲染。外部半透明遮罩点击关闭弹窗,内部 stopPropagation 阻止冒泡。

详情弹窗从右侧滑入,占据屏幕宽度 80%,使用 justifyContent(FlexAlign.End)alignItems(HorizontalAlign.End) 实现右侧对齐。内容通过 selNameselDescselTagselScoreselPrice 等安全访问方法获取选中商品信息。包含商品图标、名称、描述、标签胶囊、好评度和商品参数区域(正品保障、租售方式、配送方式三行键值对)。底部提供"加入收藏"(边框样式)和"立即租赁"(绿色填充样式)两个按钮。"立即租赁"点击后关闭详情弹窗并打开租赁弹窗,实现了从浏览到租赁的转化路径。

应用交互流程图

下图展示了应用的核心交互流程和模块间的跳转关系。

渔具集市

鱼竿专区

鱼饵窝料

钓箱配件

钓场预约

消息

我的

查看详情

立即租赁

加入收藏

立即租赁

点击商品

点击饵料

点击装备

预约钓位

发布闲置

确认发布

立即租

锁定钓位

点击删除

确认删除

再想想

渔情指数

水深柱状图

应用启动

加载渔具集市首页

用户选择标签页

浏览精选渔具

查看硬度矩阵柱状图

查看库存排行

浏览配套装备

查看渔情指数

浏览消息通知

查看个人中心

点击商品

详情弹窗

租赁弹窗

关闭弹窗

钓位预约弹窗

发布闲置弹窗

完成发布

完成租赁

完成预约

删除确认弹窗

移除消息

本周渔情数据

钓场水深对比

核心技术点对比

下表汇总了应用中各标签页和弹窗使用的关键技术点和设计模式:

功能模块 数据来源 可视化方式 弹窗交互 动画效果 核心设计特点
渔具集市 items数组 好评度进度条/统计卡片 详情弹窗/租赁弹窗 热卖标签脉动/出钓人数脉动 统计卡片+商品列表
鱼竿专区 rods数组 硬度参数柱状图 详情弹窗 火焰图标脉动 Flex双列布局+硬度柱状图
鱼饵窝料 baits数组 库存进度条/排名序号 详情弹窗 排名列表+横向滚动双视图
钓箱配件 boxes数组 容量规格柱状图 详情弹窗 工具箱图标脉动 统计卡片+双列精选布局
钓场预约 pools2数组 水深柱状图/渔情指数 钓位预约弹窗 出钓人次脉动/爆满预警脉动 渔情指数卡片+水深柱状图
消息中心 msgs数组 未读红点标识 删除确认弹窗 未读数脉动/红点脉动 鱼Emoji区分已读未读
用户中心 静态数据 统计数字卡片 发布闲置弹窗 优惠券数字脉动 钓手等级+爆护记录
发布弹窗 types/waters 标签选择器 居中模态面板 确认按钮脉动 TextInput+双选择器组合
租赁弹窗 dayChips 押金动态计算 底部弹出面板 押金脉动/赠窝料脉动 天数步进器+套餐选择
钓位弹窗 pools/slots 费用动态计算 居中模态面板 费用脉动 人数步进器+双选择器
删除弹窗 deleteIndex 图标提示 居中对话框 图标脉动 splice操作@State数组
详情弹窗 selectedItem 参数键值对/评分进度条 右侧滑入面板 价格脉动 安全访问方法+空值保护

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: boolean
}

interface TabInfo {
  name: string
  icon: string
}

interface MsgItem {
  title: string
  content: string
  time: string
  read: boolean
}

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showRent: boolean = false
  @State showPool: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State selType: number = 0
  @State selWater: number = 0
  @State selDay: number = 0
  @State rentDays: number = 3
  @State selPool: number = 0
  @State selSlot: number = 0
  @State personNum: number = 2
  @State deleteIndex: number = -1
  @State msgs: MsgItem[] = [
    { title: '渔情速报', content: '千岛湖今日鲫鱼开口好,浅滩钓位已放出 12 个', time: '11:02', read: false },
    { title: '租赁提醒', content: '您租的碳素竿明天到期,续租可免一天押金', time: '09:15', read: false },
    { title: '预约成功', content: '周六上午青龙湖 3 号钓位已锁定,请准时到场', time: '昨天', read: false },
    { title: '物流更新', content: '您的窝料套餐已从武汉仓发出,预计后天送达', time: '昨天', read: true },
    { title: '渔具保养', content: '竿节进水别硬拔,保养卡券已放入卡包', time: '前天', read: true },
    { title: '爆护榜单', content: '本周爆护榜已更新,钓友"老猫"以 46 斤夺冠', time: '前天', read: true },
    { title: '直播预告', content: '今晚 20:00 直拆新到的路亚饵,抽 20 份试用品', time: '周三', read: true },
    { title: '优惠券到账', content: '会员日渔具满 299 减 60 券已发放,7 天有效', time: '周三', read: true },
    { title: '钓场公告', content: '月湖钓场本周六举办夜钓赛,报名费 50/人', time: '周四', read: true },
    { title: '库存提醒', content: '您收藏的浮漂套装已补货,仅剩 8 套', time: '上周', read: true },
    { title: '评价有礼', content: '晒爆护图评价窝料可领 15 元券', time: '上周', read: true },
    { title: '安全提醒', content: '雷雨天气请勿在开阔水域垂钓,注意防雷', time: '上周', read: true }
  ]

  private tabs: TabInfo[] = [
    { name: '渔具集市', icon: '🎣' },
    { name: '鱼竿专区', icon: '竿' },
    { name: '鱼饵窝料', icon: '🪱' },
    { name: '钓箱配件', icon: '🧰' },
    { name: '钓场预约', icon: '🏞️' },
    { name: '消息', icon: '✉️' },
    { name: '我的', icon: '👤' }
  ]
  private types: string[] = ['手竿', '路亚竿', '海竿', '矶竿', '前打竿', '冰钓竿']
  private waters: string[] = ['江河野钓', '湖库守钓', '黑坑塘钓', '溪流飞钓']
  private dayChips: string[] = ['3天', '7天', '15天', '30天']
  private pools: string[] = ['青龙湖', '月湖夜钓场', '千岛湖', '金沙湾水库']
  private slots: string[] = ['清晨 5:00', '上午 8:00', '下午 14:00', '夜钓 19:00']

  private items: GoodsItem[] = [
    { name: '化氏一味EX手竿5.4米', price: 469, desc: '碳布吨位高 轻硬挺', score: 96, num: 5, tag: '鲫鲤通杀', hot: true },
    { name: '达瓦黑鲈路亚竿M调', price: 799, desc: '枪柄直柄可选 远投顺滑', score: 95, num: 4, tag: '路亚党', hot: true },
    { name: '光威无法一本4.5米', price: 199, desc: '新手入门 皮实耐造', score: 93, num: 6, tag: '性价比', hot: false },
    { name: '开沃精工武乾坤', price: 1280, desc: '竞技级调性 回鱼快', score: 97, num: 4, tag: '竞技款', hot: false },
    { name: '渔之源矶竿2号3.6米', price: 289, desc: '导环顺滑 绕线不炸', score: 92, num: 5, tag: '矶钓', hot: false },
    { name: '小红帽路亚轮1000型', price: 359, desc: '浅线杯 轻量化机身', score: 94, num: 3, tag: '微物轮', hot: true },
    { name: '狼王纺车轮3000型', price: 219, desc: '8+1轴承 顺滑耐用', score: 91, num: 6, tag: '通用轮', hot: false },
    { name: '达金溪流飞钓竿', price: 688, desc: '软调优雅 保护子线', score: 92, num: 2, tag: '飞钓', hot: false },
    { name: '阿卢浮漂L05枣核型', price: 128, desc: '芦苇手工 吃口清晰', score: 96, num: 9, tag: '口诀神器', hot: true },
    { name: '黄金眼夜光漂套装', price: 158, desc: '高亮电池 夜钓必备', score: 93, num: 8, tag: '夜钓', hot: false },
    { name: '天元窝料五谷杂粮', price: 45, desc: '发酵工艺 打窝留鱼', score: 94, num: 30, tag: '囤货装', hot: false },
    { name: '老坛玉米粒诱饵', price: 32, desc: '酸香发酵 草鲤最爱', score: 95, num: 40, tag: '热销', hot: true },
    { name: '化氏红薯鲤饵料', price: 38, desc: '薯香浓郁 状态雾化好', score: 93, num: 26, tag: '爆款', hot: false },
    { name: '仿生软虫路亚饵组', price: 69, desc: '20条装 T尾卷尾', score: 90, num: 18, tag: '套装', hot: false },
    { name: '亮片铁板VIB套装', price: 89, desc: '12g远投 全水层搜索', score: 92, num: 15, tag: '路亚', hot: false },
    { name: '米诺水面系拟饵', price: 78, desc: '走水自然 狗刨动作', score: 91, num: 12, tag: '翘嘴', hot: false },
    { name: '竞技钓箱28升', price: 268, desc: '加厚保温 可坐人', score: 94, num: 7, tag: '钓箱', hot: false },
    { name: '伞架炮台三件套', price: 129, desc: '铝合金 不晃不松', score: 92, num: 11, tag: '支架', hot: false },
    { name: '抄网铝合金3米', price: 98, desc: '轻量网头 不挂鳞', score: 91, num: 13, tag: '抄网', hot: false },
    { name: '鱼护防挂尼龙2.5米', price: 66, desc: '加密网眼 存鱼安全', score: 90, num: 16, tag: '配件', hot: false },
    { name: '失手绳防炸竿', price: 25, desc: '高弹橡胶 断竿保险', score: 89, num: 22, tag: '保险', hot: false },
    { name: '防晒钓鱼伞银胶', price: 158, desc: 'UPF50+ 抗风骨架', score: 93, num: 9, tag: '遮阳', hot: false },
    { name: '钓椅升降靠背款', price: 189, desc: '久坐不累 脚踩升降', score: 92, num: 8, tag: '舒适', hot: false },
    { name: '电子称重器50kg', price: 79, desc: '精准到克 爆护留念', score: 90, num: 14, tag: '记录', hot: false }
  ]
  private rods: GoodsItem[] = [
    { name: '一味EX 3.9米', price: 359, desc: '28调 混养快速回鱼', score: 96, num: 4, tag: '硬度4H', hot: true },
    { name: '一味EX 4.5米', price: 399, desc: '长身控鱼 护线不炸', score: 96, num: 5, tag: '硬度5H', hot: false },
    { name: '一味EX 5.4米', price: 469, desc: '远投打底 大物首选', score: 97, num: 5, tag: '硬度6H', hot: true },
    { name: '无法一本 3.6米', price: 159, desc: '入门结实 练竿利器', score: 93, num: 4, tag: '硬度3H', hot: false },
    { name: '无法一本 4.5米', price: 199, desc: '鲫鲤兼顾 皮实', score: 93, num: 4, tag: '硬度4H', hot: false },
    { name: '武乾坤 5.7米', price: 1280, desc: '竞技设计 飞磕神器', score: 97, num: 6, tag: '硬度8H', hot: false },
    { name: '竹山五代 4.5米', price: 139, desc: '桐木手作 复古情怀', score: 90, num: 3, tag: '硬度2H', hot: false },
    { name: '浪尖黑坑 4.8米', price: 520, desc: '黑坑专用 回鱼暴力', score: 95, num: 6, tag: '硬度7H', hot: true },
    { name: '斩鲈M调路亚竿', price: 439, desc: '泛用M调 软饵硬饵通吃', score: 94, num: 2, tag: '硬度M', hot: false },
    { name: '斩鲈H调路亚竿', price: 489, desc: '重障区 flipping首选', score: 94, num: 2, tag: '硬度H', hot: false },
    { name: '冰钓竿1.8米', price: 119, desc: '短节便携 冬钓神器', score: 91, num: 3, tag: '硬度2H', hot: false },
    { name: '前打竿7米', price: 699, desc: '滑漂远投 深水探底', score: 93, num: 2, tag: '硬度5H', hot: false }
  ]
  private baits: GoodsItem[] = [
    { name: '老坛酸香玉米', price: 32, desc: '发酵老坛 窝留双效', score: 95, num: 6, tag: '打窝', hot: true },
    { name: '五谷杂粮窝料', price: 45, desc: '续窝神器 不死窝', score: 94, num: 5, tag: '打窝', hot: false },
    { name: '红薯大鲤饵', price: 38, desc: '薯香雾化 诱钓合一', score: 93, num: 4, tag: '钓饵', hot: false },
    { name: '腥香鲫鱼饵', price: 35, desc: '冬鲫利器 状态拉糊', score: 92, num: 4, tag: '钓饵', hot: false },
    { name: '红虫拉饵伴侣', price: 26, desc: '拉红虫不脏手', score: 90, num: 3, tag: '辅助', hot: false },
    { name: '麝香酒米窝料', price: 29, desc: '酒米留鱼 聚鱼快', score: 91, num: 5, tag: '打窝', hot: false },
    { name: '果酸诱食剂', price: 19, desc: '夏季肥水 破口神助', score: 89, num: 3, tag: '小药', hot: false },
    { name: '雾化颗粒1.0', price: 22, desc: '散炮伴侣 层层雾化', score: 90, num: 4, tag: '散炮', hot: false },
    { name: '虾滑拉饵', price: 33, desc: '虾肉拉饵 罗非最爱', score: 91, num: 3, tag: '钓饵', hot: false },
    { name: '麸香底窝料', price: 28, desc: '沉底留香 久钓不散', score: 90, num: 4, tag: '打窝', hot: false }
  ]
  private boxes: GoodsItem[] = [
    { name: '竞技钓箱28L', price: 268, desc: 'EPP保温 四脚升降', score: 94, num: 28, tag: '钓箱', hot: true },
    { name: '便携钓箱18L', price: 168, desc: '轻量短途 手提舒适', score: 92, num: 18, tag: '钓箱', hot: false },
    { name: '伞架炮台套装', price: 129, desc: '万能接口 稳如泰山', score: 92, num: 3, tag: '支架', hot: false },
    { name: '夜钓灯蓝光充电款', price: 99, desc: '不惊鱼 聚光省电', score: 93, num: 12, tag: '夜钓', hot: true },
    { name: '多功能子线盒', price: 39, desc: '防锈绕线 双面收纳', score: 90, num: 8, tag: '收纳', hot: false },
    { name: '饵料开饵盆', price: 29, desc: '量杯刻度 一盆搞定', score: 89, num: 6, tag: '开饵', hot: false },
    { name: '鱼钩绑线器', price: 25, desc: '绑钩神器 新手友好', score: 91, num: 5, tag: '工具', hot: false },
    { name: '防晒钓鱼伞2.4米', price: 158, desc: '银胶防晒 双层抗风', score: 93, num: 4, tag: '遮阳', hot: false },
    { name: '防水饵料包', price: 59, desc: '分层收纳 防潮密封', score: 90, num: 7, tag: '收纳', hot: false },
    { name: '控鱼器不锈钢', price: 45, desc: '安全摘钩 不伤手', score: 91, num: 6, tag: '摘钩', hot: false }
  ]
  private pools2: GoodsItem[] = [
    { name: '青龙湖野钓区', price: 80, desc: '3号位鲫鱼连竿 圣地', score: 95, num: 3, tag: '野钓', hot: true },
    { name: '月湖夜钓场', price: 100, desc: '灯光夜钓 大物出没', score: 94, num: 5, tag: '夜钓', hot: true },
    { name: '千岛湖库湾', price: 150, desc: '湖库巨物 需提前约', score: 96, num: 12, tag: '湖库', hot: false },
    { name: '金沙湾水库', price: 120, desc: '休闲大库 环境清幽', score: 93, num: 8, tag: '湖库', hot: false },
    { name: '城市黑坑一号', price: 200, desc: '高密度塘 正钓爽', score: 92, num: 2, tag: '黑坑', hot: false },
    { name: '城市黑坑二号', price: 180, desc: '偷驴好去处 鱼口滑', score: 90, num: 2, tag: '黑坑', hot: false },
    { name: '溪流飞钓点', price: 60, desc: '山涧冷水鱼 清爽', score: 91, num: 1, tag: '溪流', hot: false },
    { name: '江湾筏钓平台', price: 130, desc: '筏钓翘嘴 深水区', score: 92, num: 6, tag: '筏钓', hot: false },
    { name: '亲子休闲渔场', price: 50, desc: '小孩免费 鱼好钓', score: 89, num: 1, tag: '亲子', hot: false },
    { name: '竞技练竿塘', price: 90, desc: '按小时计 回鱼快', score: 93, num: 3, tag: '练竿', hot: false }
  ]

  selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }
  selDesc(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.desc
  }
  selTag(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.tag
  }
  selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }
  selScore(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.score
  }
  maxOfNum(list: GoodsItem[]): number {
    let m: number = 0
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }
  unreadCount(): number {
    let c: number = 0
    for (let i = 0; i < this.msgs.length; i++) {
      if (!this.msgs[i].read) {
        c++
      }
    }
    return c
  }

  @Builder buildHeader() {
    Column() {
      Row() {
        Column() {
          Row() {
            Text('📍').fontSize(12)
            Text('杭州 · 西湖区').fontSize(12).fontColor(Color.White).margin({ left: 4 })
            Text('›').fontSize(12).fontColor('rgba(255,255,255,0.7)').margin({ left: 4 })
          }
          .alignItems(VerticalAlign.Center)

          Text('京东垂钓渔具馆').fontSize(19).fontColor(Color.White).fontWeight(FontWeight.Bold).margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('🎣').fontSize(20)
          Text('爆护榜').fontSize(9).fontColor('rgba(255,255,255,0.8)').margin({ top: 2 })
        }
        .width(52)
        .onClick(() => {
          this.currentTab = 0
        })

        Column() {
          Text('✉️').fontSize(20)
          if (this.unreadCount() > 0) {
            Text(this.unreadCount().toString())
              .fontSize(8)
              .fontColor(Color.White)
              .backgroundColor('#FF3B30')
              .borderRadius(7)
              .padding({ left: 4, right: 4 })
              .position({ x: 14, y: -6 })
              .scale({ x: 1.15, y: 1.15 })
              .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
          }
        }
        .width(52)
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('渔情:今日 18-24℃ 微风').fontSize(11).fontColor('#E3F6ED')
          Text('鲫鱼开口期 · 浅滩 1.5-2.5 米').fontSize(10).fontColor('rgba(255,255,255,0.75)').margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('今日宜钓').fontSize(11).fontColor(Color.White).backgroundColor('#558B2F').borderRadius(10).padding({ left: 8, right: 8, top: 4, bottom: 4 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor('#0277BD')
  }

  @Builder buildTab1() {
    Column() {
      Row({ space: 8 }) {
        Column() {
          Text('🎣').fontSize(22)
          Text('爆护率').fontSize(10).fontColor('#5C6280').margin({ top: 4 })
          Text('78%').fontSize(15).fontColor('#0277BD').fontWeight(FontWeight.Bold).margin({ top: 2 })
        }
        .layoutWeight(1)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding({ top: 10, bottom: 10 })

        Column() {
          Text('🏆').fontSize(22)
          Text('本周鱼获').fontSize(10).fontColor('#5C6280').margin({ top: 4 })
          Text('12.6吨').fontSize(15).fontColor('#558B2F').fontWeight(FontWeight.Bold).margin({ top: 2 })
        }
        .layoutWeight(1)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding({ top: 10, bottom: 10 })

        Column() {
          Text('👥').fontSize(22)
          Text('今日出钓').fontSize(10).fontColor('#5C6280').margin({ top: 4 })
          Text('3269人').fontSize(15).fontColor('#D84315').fontWeight(FontWeight.Bold).margin({ top: 2 })
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 750, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding({ top: 10, bottom: 10 })
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 12 })

      Row() {
        Text('为你精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        Text('按销量排序'.slice(0, 6)).fontSize(10).fontColor('#9AA0B5').margin({ left: 8 })
        Text('筛选 ›').fontSize(10).fontColor('#0277BD').margin({ left: 12 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      ForEach(this.items, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text(g.tag.slice(0, 1)).fontSize(24).fontColor(Color.White)
          }
          .width(72)
          .height(72)
          .borderRadius(12)
          .backgroundColor(i % 3 === 0 ? '#0277BD' : (i % 3 === 1 ? '#558B2F' : '#0288D1'))
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Column() {
                Text(g.tag).fontSize(9).fontColor('#558B2F').backgroundColor('#E3F6ED').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
              }
              if (g.hot) {
                Column() {
                  Text('热卖').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
                }
                .margin({ left: 6 })
                .scale({ x: 1.08, y: 1.08 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .margin({ top: 5 })
            .alignItems(VerticalAlign.Center)

            Stack({ alignContent: Alignment.Start }) {
              Column().width('100%').height(6).backgroundColor('#F0F0F5').borderRadius(3)
              Column().width(g.score + '%').height(6).backgroundColor('#0277BD').borderRadius(3)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Text('¥' + g.price).fontSize(16).fontColor('#D84315').fontWeight(FontWeight.Bold)
            Column() {
              Text('立即租赁').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#558B2F')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 8 })
            .onClick(() => {
              this.selectedItem = g
              this.rentDays = 3
              this.showRent = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.selectedItem = g
          this.showDetail = true
        })
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab2() {
    Column() {
      Text('鱼竿硬度矩阵').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.rods, (g: GoodsItem, i: number) => {
          Column() {
            Row() {
              Column() {
                Text('🎣').fontSize(20)
              }
              .width(44)
              .height(44)
              .borderRadius(22)
              .backgroundColor(i % 2 === 0 ? '#E3F2FA' : '#E3F6ED')
              .justifyContent(FlexAlign.Center)

              Column() {
                Text(g.tag).fontSize(10).fontColor('#0277BD').fontWeight(FontWeight.Bold)
                Text('好评 ' + g.score + '%').fontSize(9).fontColor('#8A8FA3').margin({ top: 3 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Text(g.name).fontSize(12).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
            Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text('¥' + g.price).fontSize(14).fontColor('#D84315').fontWeight(FontWeight.Bold)
              if (g.hot) {
                Text('🔥').fontSize(11).margin({ left: 6 })
                  .scale({ x: 1.15, y: 1.15 })
                  .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .width('100%')
            .margin({ top: 6 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .width('48%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

      Column() {
        Text('硬度参数柱状图').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Row({ space: 10 }) {
          ForEach(this.rods, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.rods) * 90)
                .backgroundColor('#8BD4A8')
                .borderRadius(4)
              Text(g.tag.slice(2, 4)).fontSize(8).fontColor('rgba(255,255,255,0.85)').margin({ top: 4 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .backgroundColor('#2E5E3A')
      .borderRadius(14)
      .padding(14)
      .margin({ left: 12, right: 12, top: 4 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab3() {
    Column() {
      Text('鱼饵窝料 · 库存排行').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        ForEach(this.baits, (g: GoodsItem, i: number) => {
          Row() {
            Text((i + 1).toString()).fontSize(12).fontColor(Color.White).backgroundColor(i < 3 ? '#D84315' : '#9AA0B5').borderRadius(10).width(20).height(20).textAlign(TextAlign.Center)

            Column()
              .width(4)
              .height(44)
              .backgroundColor(i % 3 === 0 ? '#558B2F' : (i % 3 === 1 ? '#0277BD' : '#D84315'))
              .borderRadius(2)
              .margin({ left: 10 })

            Column() {
              Row() {
                Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
                Text(g.tag).fontSize(9).fontColor('#558B2F').backgroundColor('#E3F6ED').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
              }
              .alignItems(VerticalAlign.Center)

              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(7).backgroundColor('#F0F0F5').borderRadius(4)
                Column().width(g.num / this.maxOfNum(this.baits) * 100 + '%').height(7).backgroundColor('#0277BD').borderRadius(4)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 10 })

            Column() {
              Text('¥' + g.price).fontSize(15).fontColor('#D84315').fontWeight(FontWeight.Bold)
              Text('剩 ' + g.num * 5 + ' 件').fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

      Text('小药添加剂 · 横向速览').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 10, bottom: 8 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.baits, (g: GoodsItem) => {
            Column() {
              Column() {
                Text('🪱').fontSize(26)
              }
              .width(68)
              .height(68)
              .borderRadius(12)
              .backgroundColor('#E3F6ED')
              .justifyContent(FlexAlign.Center)

              Text(g.name).fontSize(11).fontColor('#212121').margin({ top: 8 }).maxLines(1)
              Text('¥' + g.price).fontSize(13).fontColor('#D84315').fontWeight(FontWeight.Bold).margin({ top: 4 })
              Text('好评 ' + g.score + '%').fontSize(9).fontColor('#8A8FA3').margin({ top: 3 })
            }
            .width(104)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .padding(10)
            .onClick(() => {
              this.selectedItem = g
              this.showDetail = true
            })
          }, (g: GoodsItem) => g.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab4() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('钓箱配件仓').fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Bold)
            Text('钓箱 · 支架 · 夜钓灯 · 收纳').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('🧰').fontSize(30)
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row({ space: 8 }) {
          ForEach(this.boxes, (g: GoodsItem) => {
            Column() {
              Column()
                .width(14)
                .height(g.num / this.maxOfNum(this.boxes) * 70)
                .backgroundColor('#4FC3F7')
                .borderRadius(3)
              Text(g.tag).fontSize(8).fontColor('rgba(255,255,255,0.8)').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 14 })

        Text('容量/规格柱状图(升 / 厘米)').fontSize(9).fontColor('rgba(255,255,255,0.55)').margin({ top: 8 })
      }
      .width('100%')
      .backgroundColor('#01579B')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('装备双列精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.boxes, (g: GoodsItem, i: number) => {
          Column() {
            Column() {
              Text('🧰').fontSize(26)
            }
            .width('100%')
            .height(70)
            .borderRadius(10)
            .backgroundColor(i % 2 === 0 ? '#E3F2FA' : '#EFEBE0')
            .justifyContent(FlexAlign.Center)

            Text(g.name).fontSize(12).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
            Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text('¥' + g.price).fontSize(14).fontColor('#D84315').fontWeight(FontWeight.Bold)
              Text('租').fontSize(10).fontColor(Color.White).backgroundColor('#558B2F').borderRadius(4).padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 6 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .width('48%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }



总结

在这里插入图片描述

本文对基于 HarmonyOS 声明式 UI 框架开发的"京东垂钓渔具馆"应用进行了全面的代码级深度解析。该应用通过三个接口定义了商品、标签和消息的数据模型,通过十五个 @State 状态变量管理七个标签页切换和五个弹窗显隐,通过十二个 @Builder 方法封装了头部栏、七个标签页、底部导航栏和五个弹窗的 UI 构建。应用预置了五组商品数据集合(通用渔具、鱼竿、鱼饵、钓箱配件、钓场),共计六十六项产品数据,覆盖了从入门级到竞技级的完整渔具产品矩阵和十种不同类型的钓场。特别值得关注的是,同一 GoodsItem 接口在不同标签页中承载了不同语义的数据(库存件数、硬度等级、库存包数、容量规格、水深米数),体现了接口复用的设计思路。

在数据可视化方面,应用在多个标签页中使用了不同类型的柱状图:渔具集市的好评度进度条、鱼竿专区的硬度参数柱状图、鱼饵窝料的库存进度条、钓箱配件的容量规格柱状图和钓场预约的水深柱状图。所有柱状图都使用 Column 组件配合归一化计算(g.num / this.maxOfNum(list) * 基准高度)实现,完全使用 ArkTS 原生组件构建。maxOfNum 通用工具方法被四个标签页的柱状图共用,体现了代码复用的设计原则。此外,钓场预约标签页的"渔情指数卡片"展示了综合渔情指数、出钓人次和单尾最重记录三个维度的数据,是垂钓行业特色数据可视化的典型实现。头部栏集成的地理位置定位和实时渔情信息(温度、风力、鱼情判断、"今日宜钓"标签),进一步增强了应用的场景化服务能力。

在交互设计方面,应用采用了居中模态和底部弹出两种弹窗模式:发布闲置和钓位预约使用居中模态,适合表单和选择类操作;租赁弹窗使用底部弹出,适合流程操作类交互。钓位预约弹窗是应用最具垂钓特色的设计,提供了钓场选择、时段选择和同行人数调节三个维度的选择,费用计算公式为 this.personNum * 80(每人 80 元含钓位费和窝料)。租赁弹窗的"赠窝料"标签和 60 天上限的天数调节器,都体现了垂钓场景的行业特色。删除操作通过 splice 方法直接修改 @State 数组实现列表项移除,利用 ArkTS 的响应式机制自动更新界面。整体而言,该应用代码结构清晰、功能覆盖全面、行业特色鲜明,是 HarmonyOS 声明式 UI 在户外运动行业电商应用开发中的优秀实践案例。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐