引言

在这里插入图片描述

随着二次元文化在国内的蓬勃发展,“谷子”(goods 的谐音,指动漫周边商品)交易已成为一个活跃的细分市场。吧唧(徽章)、立牌、色纸、票谷等周边商品的收藏与交易需求日益旺盛,但传统交易渠道分散在闲鱼、微博超话等非专业平台,存在真伪难辨、价格不透明、交易风险高等痛点。本应用以 HarmonyOS ArkTS API 24 为技术底座,构建了一个专业的二次元周边交易平台——谷子集市,覆盖了今日上新、行情走势、求购发布、出谷寄售、订单管理和个人中心六大核心业务场景。

从技术架构层面来看,本应用采用了 ArkTS 的声明式组件体系,主入口组件 GPage 通过 @Entry@Component 装饰器注册为应用根组件,内部维护 activeTab 状态变量驱动六个子组件的条件渲染。与简单的页面切换不同,每个子组件(TodayTabMarketTabWantedTabSellTabGOrderTabGMineTab)都是功能完备的独立模块,各自管理着自己的 @State 状态集合和业务逻辑方法。这种架构不仅保证了模块间的低耦合高内聚,还使得每个 Tab 可以独立进行状态更新而互不影响。应用的视觉主题采用活力橙 #FF6A00 搭配靛蓝 #2563EB 的撞色方案,底色为 #F5F7FF 的淡蓝白色,营造出二次元特有的活力感。

在业务设计层面,应用创新性地引入了"谷子指数"概念——类似于股票指数的综合性行情指标,配合 7 日价格趋势柱状图和涨跌幅数据,将周边交易从单纯的买卖行为升级为具备投资视角的行情分析。盲盒机功能通过 Math.random() 实现了 SSR/SR/R 三档概率抽取(SSR 6%、SR 24%、R 70%),还原了二次元谷子圈"抽谷"的随机乐趣。出谷模块提供了完整的寄售管理流程,包括上架、改价、议价回复、下架等操作,其中议价功能支持一键折扣(95 折/9 折/85 折/8 折)和手动输入两种回价方式,兼顾了效率和灵活性。订单模块集成了物流轨迹时间线展示和售后申请流程,构建了完整的交易闭环。

逐段代码分析

在这里插入图片描述

一、数据模型与类型定义层

应用首先定义了涵盖二次元周边交易全领域的接口类型集合,每个接口对应一个业务实体。

interface IpChip {
  name: string
  count: number
}

interface GoodsItem {
  id: number
  name: string
  ip: string
  type: string
  grade: string
  price: number
  originPrice: number
  stock: number
  sold: number
  hot: boolean
  color: string
}

interface PriceTrend {
  day: string
  price: number
}

interface MarketRow {
  id: number
  name: string
  ip: string
  lastPrice: number
  change: number
  deals: number
  grade: string
}

interface WantedRow {
  id: number
  ip: string
  type: string
  budget: string
  note: string
  count: number
}

interface SellRow {
  id: number
  name: string
  grade: string
  price: number
  status: string
  statusColor: string
  asks: number
  views: number
}

上述接口定义体现了二次元周边交易领域的业务特征。GoodsItem 接口中的 grade 字段表示商品稀有度等级(SSR/SR/R),这是二次元谷子文化的核心概念。originPriceprice 的组合实现了划线价展示。MarketRow 接口中的 change 字段为涨跌幅百分比,正值表示上涨、负值表示下跌,配合 deals 成交量字段构成了行情分析的基础数据。WantedRow 接口定义了求购单结构,budget 采用字符串类型存储预算区间(如"¥50-80"),note 字段用于补充品相和角色要求。SellRow 接口中的 statusstatusColor 配对使用,通过颜色区分"寄售中"“已预定”“已下架”"已售出"等状态,asksviews 分别记录议价条数和浏览量,为卖家提供运营数据参考。

二、静态数据与档位颜色配置

在这里插入图片描述

const IP_CHIPS: IpChip[] = [
  { name: '全部 IP', count: 1024 },
  { name: '时光代理人', count: 186 },
  { name: '咒术回战', count: 204 },
  { name: '排球少年', count: 168 },
  { name: '天官赐福', count: 122 },
  { name: '文豪野犬', count: 96 }
]

const TODAY_GOODS: GoodsItem[] = [
  { id: 1, name: '程小时 吧唧·雨夜', ip: '时光代理人', type: '吧唧', grade: 'SSR',
    price: 68, originPrice: 128, stock: 42, sold: 1682, hot: true, color: '#FFB88C' },
  { id: 2, name: '五条悟 Q版立牌', ip: '咒术回战', type: '立牌', grade: 'SSR',
    price: 88, originPrice: 158, stock: 26, sold: 2341, hot: true, color: '#A5C8F0' },
  { id: 3, name: '影山飞雄 拼图吧唧', ip: '排球少年', type: '吧唧', grade: 'SR',
    price: 35, originPrice: 68, stock: 88, sold: 3210, hot: false, color: '#BFD8A8' },
  { id: 4, name: '花城 色纸·血雨探花', ip: '天官赐福', type: '色纸', grade: 'SSR',
    price: 128, originPrice: 228, stock: 12, sold: 862, hot: true, color: '#E8A0A8' },
  { id: 5, name: '太宰治 亚克力钥匙扣', ip: '文豪野犬', type: '周边', grade: 'SR',
    price: 42, originPrice: 78, stock: 65, sold: 1207, hot: false, color: '#C5B3E8' },
  { id: 6, name: '陆光 镭射票根', ip: '时光代理人', type: '票谷', grade: 'R',
    price: 19, originPrice: 36, stock: 210, sold: 4560, hot: false, color: '#A8D8D8' }
]

const PRICE_TREND: PriceTrend[] = [
  { day: '8/18', price: 52 },
  { day: '8/19', price: 61 },
  { day: '8/20', price: 58 },
  { day: '8/21', price: 73 },
  { day: '8/22', price: 82 },
  { day: '8/23', price: 96 },
  { day: '8/24', price: 88 }
]

const GRADE_COLORS: string[] = ['#FF6A00', '#2563EB', '#8B5CF6']

IP_CHIPS 定义了 IP 分类标签,涵盖了时光代理人、咒术回战、排球少年等热门 IP。TODAY_GOODS 是今日上新页面的数据源,每条记录包含了商品名称、所属 IP、品类、稀有度等级、当前价格、原价、库存、已售数量和热度标记。color 字段为每个商品分配了一个独特的背景色值,在卡片渲染时作为商品图片占位区的背景色,实现了视觉上的快速区分。PRICE_TREND 数组提供了 7 日价格趋势数据,用于行情页的柱状图渲染。GRADE_COLORS 数组将稀有度等级映射为颜色——SSR 对应橙色 #FF6A00、SR 对应靛蓝 #2563EB、R 对应紫色 #8B5CF6,这种配色方案贯穿了整个应用的档位标识系统。

三、档位颜色函数与趋势图工具函数

在这里插入图片描述

function gradeColor(g: string): string {
  if (g === 'SSR') {
    return '#FF6A00'
  }
  if (g === 'SR') {
    return '#2563EB'
  }
  return '#8B5CF6'
}

function trendBarH(p: number): number {
  if (p >= 90) {
    return 88
  }
  if (p >= 75) {
    return 72
  }
  if (p >= 60) {
    return 56
  }
  if (p >= 45) {
    return 40
  }
  return 28
}

@Builder
function modalOverlay(onClose: () => void) {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(20,24,40,0.55)')
    .onClick(() => {
      onClose()
    })
}

gradeColor 函数是整个应用的档位颜色核心映射器,接收档位字符串返回对应的颜色值。与静态数组 GRADE_COLORS 不同,函数版本提供了更灵活的调用方式,可以直接传入任意档位字符串获取颜色。trendBarH 函数根据价格值返回柱状图高度,将价格区间映射为 28-88 像素的高度值,价格越高柱子越高。modalOverlay 是公共的模态遮罩 @Builder 函数,使用 rgba(20,24,40,0.55) 的半透明深色背景,点击时触发传入的 onClose 回调关闭弹窗。这个遮罩组件被所有弹窗复用,保证了交互行为的一致性。

四、今日谷 Tab——TodayTab 组件

在这里插入图片描述

今日谷 Tab 是应用的首屏页面,展示每日上新商品列表、盲盒机入口和闪购预告。

@Component
export struct TodayTab {
  @State ipIdx: number = 0
  @State showBuy: boolean = false
  @State showLuck: boolean = false
  @State showFlash: boolean = false
  @State buyNum: number = 1
  @State selGoods: GoodsItem = TODAY_GOODS[0]
  @State luckyGrade: string = ''

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 10 }) {
          Row({ space: 10 }) {
            Column({ space: 3 }) {
              Text('今日吃谷日')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('每日 10:00 / 20:00 两波上新')
                .fontSize(11)
                .fontColor('#FFE3CF')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Text('盲盒机')
              .fontSize(12)
              .fontColor('#2563EB')
              .padding({ left: 12, right: 12, top: 7, bottom: 7 })
              .borderRadius(14)
              .backgroundColor('#FFFFFF')
              .onClick(() => {
                this.showLuck = true
              })
          }
          .width('100%')

          Row({ space: 8 }) {
            Column({ space: 2 }) {
              Text('16:42:08')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('晚场倒计时')
                .fontSize(9)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            .padding({ top: 8, bottom: 8 })
            .borderRadius(10)
            .backgroundColor('rgba(255,255,255,0.2)')
            .onClick(() => {
              this.showFlash = true
            })
            Column({ space: 2 }) {
              Text('2.6 万')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('今日吃谷人数')
                .fontSize(9)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('5 折')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('晚场专区')
                .fontSize(9)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            .onClick(() => {
              this.showFlash = true
            })
          }
          .width('100%')
        }
        .padding(14)
        .borderRadius(14)
        .linearGradient({
          angle: 135,
          colors: [['#FF6A00', 0.0], ['#2563EB', 1.0]]
        })
        .width('100%')

TodayTab 组件头部使用了从橙色到靛蓝的 135 度渐变背景,呼应了应用的双主色撞色主题。头部区域分为两层:上层是标题"今日吃谷日"与盲盒机入口按钮的左右布局,下层是三列等宽的统计数据栏(晚场倒计时、今日吃谷人数、晚场专区折扣)。倒计时区域使用了 rgba(255,255,255,0.2) 的半透明白色背景,点击后打开闪购预告弹窗。luckyGrade 状态变量初始为空字符串,在盲盒机弹窗中根据随机结果赋值为 ‘SSR’、‘SR’ 或 ‘R’,驱动弹窗内文案和视觉的动态变化。

五、商品瀑布流卡片与购买弹窗

在这里插入图片描述

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
  ForEach(TODAY_GOODS, (g: GoodsItem) => {
    Column({ space: 8 }) {
      Stack({  }) {
        Column()
          .width('100%')
          .height(120)
          .borderRadius(10)
          .backgroundColor(g.color)
        Text(g.grade)
          .fontSize(10)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .padding({ left: 7, right: 7, top: 3, bottom: 3 })
          .borderRadius({ topLeft: 10, bottomRight: 10 })
          .backgroundColor(gradeColor(g.grade))
        if (g.hot) {
          Text('HOT')
            .fontSize(9)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding({ left: 6, right: 6, top: 3, bottom: 3 })
            .borderRadius(4)
            .backgroundColor('#EF4444')
            .margin(6)
        }
      }
      .width('100%')

      Column({ space: 4 }) {
        Text(g.name)
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1F2937')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(g.ip + ' · ' + g.type)
          .fontSize(10)
          .fontColor('#94A3B8')
        Row() {
          Text('¥' + g.price)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6A00')
          Text('¥' + g.originPrice)
            .fontSize(10)
            .fontColor('#CBD5E1')
            .decoration({ type: TextDecorationType.LineThrough })
          Row()
            .layoutWeight(1)
          Text('抢')
            .fontSize(12)
            .fontColor('#FFFFFF')
            .padding({ left: 12, right: 12, top: 5, bottom: 5 })
            .borderRadius(12)
            .backgroundColor('#FF6A00')
            .onClick(() => {
              this.selGoods = g
              this.showBuy = true
            })
        }
        .width('100%')
        Text('已售 ' + g.sold + ' · 库存 ' + g.stock)
          .fontSize(9)
          .fontColor('#94A3B8')
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
    }
    .padding(10)
    .borderRadius(12)
    .backgroundColor('#FFFFFF')
    .width('48.5%')
    .margin({ bottom: 10 })
  }, (g: GoodsItem) => g.id.toString())
}
.width('100%')

商品列表使用 Flex 布局配合 FlexWrap.WrapSpaceBetween 实现了双列瀑布流效果。每个卡片宽度设为 48.5%,通过 margin({ bottom: 10 }) 控制垂直间距。卡片顶部的商品图片区域使用 Stack 组件实现层叠效果——底层是纯色背景块(颜色来自 g.color),上层左上角放置档位标签(使用 gradeColor 函数动态着色,并设置 borderRadius 只圆角左上和右下),右上角条件渲染 HOT 标签。价格区域同时显示当前价格(橙色加粗)和原价(灰色带删除线,通过 TextDecorationType.LineThrough 实现),划线价设计是电商场景的经典交互。点击"抢"按钮后将选中商品赋值给 selGoods 并打开购买弹窗。

六、盲盒机抽谷弹窗

在这里插入图片描述

if (this.showLuck) {
  modalOverlay(() => {
    this.showLuck = false
  })
  Column({ space: 14 }) {
    Text('谷子盲盒机')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1F2937')
    Text('¥39/抽 · SSR 概率 6%')
      .fontSize(12)
      .fontColor('#94A3B8')
    Row({ space: 12 }) {
      Column({ space: 5 }) {
        Column()
          .width(76)
          .height(76)
          .borderRadius(12)
          .backgroundColor(this.luckyGrade === '' ? '#FDBA74' : '#FF6A00')
        Text(this.luckyGrade === '' ? '?' : this.luckyGrade)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF6A00')
      }
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)

    Text(this.luckyGrade === 'SSR' ? '欧皇附体!抽中 SSR 吧唧!' : (this.luckyGrade === 'SR' ? '不错哦,SR 入手!' : (this.luckyGrade === 'R' ? '再接再厉~' : '点击下方按钮开始抽')))
      .fontSize(12)
      .fontColor('#2563EB')

    Row({ space: 10 }) {
      Text('抽一次 ¥39')
        .fontSize(13)
        .fontColor('#FFFFFF')
        .padding({ left: 20, right: 20, top: 9, bottom: 9 })
        .borderRadius(18)
        .backgroundColor('#2563EB')
        .onClick(() => {
          const r: number = Math.random()
          if (r < 0.06) {
            this.luckyGrade = 'SSR'
          } else if (r < 0.3) {
            this.luckyGrade = 'SR'
          } else {
            this.luckyGrade = 'R'
          }
        })
      Text('收手')
        .fontSize(13)
        .fontColor('#FF6A00')
        .padding({ left: 20, right: 20, top: 9, bottom: 9 })
        .borderRadius(18)
        .backgroundColor('#FFF3E8')
        .onClick(() => {
          this.showLuck = false
        })
    }
  }
  .padding(20)
  .borderRadius(16)
  .backgroundColor('#FFFFFF')
  .alignItems(HorizontalAlign.Center)
  .width('84%')
}

盲盒机弹窗是今日谷 Tab 最具趣味性的功能。初始状态下,抽谷区域显示一个橙色色块和问号"?“。点击"抽一次"按钮后,通过 Math.random() 生成一个 0-1 的随机数:小于 0.06(6%概率)抽中 SSR,0.06-0.3 之间(24%概率)抽中 SR,大于 0.3(70%概率)抽中 R。抽中后,色块变为更深的橙色(#FF6A00),显示档位文字,并动态更新提示文案——SSR 显示"欧皇附体”,SR 显示"不错哦",R 显示"再接再厉"。这个概率设计忠实还原了二次元游戏中的抽卡机制,"收手"按钮的文案设计也充满了二次元圈子的自嘲幽默。

七、行情 Tab——MarketTab 组件

行情 Tab 提供了谷子指数、7 日价格趋势图和热门成交列表。

@Component
export struct MarketTab {
  @State ipIdx: number = 0
  @State showDeal: boolean = false
  @State showTip: boolean = false
  @State selRow: MarketRow = MARKET_ROWS[0]

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 12 }) {
          Row() {
            Text('谷子指数 1186.4')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            Text('+2.8%')
              .fontSize(11)
              .fontColor('#FFFFFF')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .borderRadius(4)
              .backgroundColor('#EF4444')
            Row()
              .layoutWeight(1)
            Text('7 日')
              .fontSize(10)
              .fontColor('#94A3B8')
          }
          .width('100%')

          Row({ space: 12 }) {
            ForEach(PRICE_TREND, (t: PriceTrend) => {
              Column({ space: 4 }) {
                Text('¥' + t.price)
                  .fontSize(8)
                  .fontColor(t.price >= 85 ? '#FF6A00' : '#94A3B8')
                Column()
                  .width(20)
                  .height(trendBarH(t.price))
                  .borderRadius({ topLeft: 4, topRight: 4 })
                  .linearGradient({
                    angle: 180,
                    colors: t.price >= 85
                      ? [['#FF9A4D', 0.0], ['#FF6A00', 1.0]]
                      : [['#93C5FD', 0.0], ['#2563EB', 1.0]]
                  })
                Text(t.day)
                  .fontSize(8)
                  .fontColor('#94A3B8')
              }
              .alignItems(HorizontalAlign.Center)
            }, (t: PriceTrend) => t.day)
          }
          .width('100%')
          .padding({ top: 4 })
        }
        .padding(12)
        .borderRadius(14)
        .backgroundColor('#FFFFFF')
        .width('100%')

行情头部区域展示了"谷子指数 1186.4"和涨幅标签"+2.8%",涨幅使用红色背景标签突出显示。下方的 7 日趋势柱状图是该组件的核心可视化元素。每个柱子的高度由 trendBarH(t.price) 动态计算,颜色根据价格阈值进行条件分支——价格大于等于 85 的柱子使用橙色渐变(从 #FF9A4D#FF6A00),低于 85 的使用蓝色渐变(从 #93C5FD#2563EB)。柱子顶部还标注了价格数值,同样根据价格阈值变色。这种双色柱状图设计使得用户能够直观地识别高价日和低价日,价格波动趋势一目了然。热门成交列表中,每条记录的涨跌幅通过 row.change > 0 ? '#EF4444' : '#16A34A' 三元表达式决定颜色——上涨为红色、下跌为绿色,符合国内股市的配色习惯。

八、求购 Tab——WantedTab 组件与 CRUD 操作

求购 Tab 实现了求购单的完整增删改查(CRUD)操作,是该应用中业务逻辑最复杂的模块。

@Component
export struct WantedTab {
  @State showPub: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State editIdx: number = -1
  @State delIdx: number = -1
  @State inputIp: string = ''
  @State inputType: string = '吧唧'
  @State inputBudget: string = ''
  @State inputNote: string = ''
  @State typeIdx: number = 0
  @State wantedList: WantedRow[] = [
    { id: 1, ip: '时光代理人', type: '吧唧', budget: '¥50-80', note: '要程小时雨夜,品相 9 成新以上', count: 2 },
    { id: 2, ip: '天官赐福', type: '色纸', budget: '¥100-150', note: '花城血雨探花,可小刀', count: 1 },
    { id: 3, ip: '排球少年', type: '立牌', budget: '¥30-60', note: '影山/及川都可,量大收', count: 4 }
  ]

  publishWanted() {
    const r: WantedRow[] = []
    for (let i = 0; i < this.wantedList.length; i++) {
      r.push(this.wantedList[i])
    }
    r.push({
      id: this.wantedList.length + 1,
      ip: this.inputIp === '' ? '咒术回战' : this.inputIp,
      type: this.inputType,
      budget: this.inputBudget === '' ? '¥40-100' : this.inputBudget,
      note: this.inputNote === '' ? '长期有效,价优可秒' : this.inputNote,
      count: 1
    })
    this.wantedList = r
    this.showPub = false
  }

  editWanted() {
    const r: WantedRow[] = []
    for (let i = 0; i < this.wantedList.length; i++) {
      if (i === this.editIdx) {
        r.push({
          id: this.wantedList[i].id,
          ip: this.inputIp === '' ? this.wantedList[i].ip : this.inputIp,
          type: this.inputType === '' ? this.wantedList[i].type : this.inputType,
          budget: this.inputBudget === '' ? this.wantedList[i].budget : this.inputBudget,
          note: this.inputNote === '' ? this.wantedList[i].note : this.inputNote,
          count: this.wantedList[i].count
        })
      } else {
        r.push(this.wantedList[i])
      }
    }
    this.wantedList = r
    this.showEdit = false
  }

  removeWanted() {
    const r: WantedRow[] = []
    for (let i = 0; i < this.wantedList.length; i++) {
      if (i !== this.delIdx) {
        r.push(this.wantedList[i])
      }
    }
    this.wantedList = r
    this.showDel = false
  }

WantedTab 组件的 wantedList 是一个可变状态数组,初始包含 3 条求购记录。三个方法 publishWantededitWantedremoveWanted 分别实现了求购单的新增、编辑和删除操作。这三个方法都采用了相同的不可变更新模式——先创建一个空数组 r,遍历原数组将元素复制到新数组中,在遍历过程中根据条件进行插入、替换或跳过操作,最后将新数组赋值给 this.wantedList 触发 UI 更新。这种模式虽然在代码量上略显冗长,但保证了状态更新的可预测性——每次更新都产生全新的数组引用,避免了直接修改原数组可能导致的引用比较失效问题。publishWanted 方法中还对空输入进行了默认值兜底处理,当用户未填写 IP、预算或备注时自动填充合理的默认值。

九、出谷 Tab——SellTab 组件与寄售管理

出谷 Tab 提供了完整的卖家寄售管理功能,包括上架、改价、议价回复和下架。

@Component
export struct SellTab {
  @State sellList: SellRow[] = [
    { id: 1, name: '程小时 吧唧·雨夜', grade: 'SSR', price: 68, status: '寄售中', statusColor: '#FF6A00', asks: 12, views: 863 },
    { id: 2, name: '五条悟 Q版立牌', grade: 'SSR', price: 88, status: '寄售中', statusColor: '#FF6A00', asks: 23, views: 1521 },
    { id: 3, name: '影山飞雄 拼图吧唧', grade: 'SR', price: 35, status: '已预定', statusColor: '#2563EB', asks: 5, views: 402 },
    { id: 4, name: '花城 色纸·血雨探花', grade: 'SSR', price: 128, status: '已下架', statusColor: '#9CA3AF', asks: 0, views: 98 },
    { id: 5, name: '太宰治 亚克力钥匙扣', grade: 'SR', price: 42, status: '已售出', statusColor: '#16A34A', asks: 0, views: 655 },
    { id: 6, name: '陆光 镭射票根', grade: 'R', price: 19, status: '寄售中', statusColor: '#FF6A00', asks: 8, views: 231 }
  ]
  @State showSell: boolean = false
  @State showBargain: boolean = false
  @State showOff: boolean = false
  @State showEdit: boolean = false
  @State showAsk: boolean = false
  @State bargainIdx: number = -1
  @State offIdx: number = -1
  @State editIdx: number = -1
  @State inputName: string = ''
  @State inputPrice: string = ''
  @State editPrice: string = ''
  @State inputGrade: string = 'SSR'
  @State gradeIdx: number = 0
  @State typeIdx: number = 0
  @State sellType: string = '吧唧'
  @State bargainPrice: string = ''

  applySell() {
    const r: SellRow[] = []
    for (let i = 0; i < this.sellList.length; i++) {
      r.push(this.sellList[i])
    }
    r.unshift({
      id: this.sellList.length + 1,
      name: this.inputName === '' ? '未命名谷子' : this.inputName,
      grade: this.inputGrade,
      price: Number(this.inputPrice === '' ? '0' : this.inputPrice),
      status: '寄售中',
      statusColor: '#FF6A00',
      asks: 0,
      views: 0
    })
    this.sellList = r
    this.showSell = false
    this.inputName = ''
    this.inputPrice = ''
  }

  setDiscount(rate: number) {
    const p: number = this.sellList[this.bargainIdx].price
    this.bargainPrice = String(Math.round(p * rate))
  }

SellTab 是该应用状态最丰富的组件,拥有 16 个 @State 变量。sellList 初始包含 6 条寄售记录,涵盖四种状态。applySell 方法使用 r.unshift() 将新上架商品插入到数组头部,使得新上架的商品在列表中优先展示。上架弹窗中提供了档位选择(SSR/SR/R 三档通过 gradeIdx 控制)、品类选择(吧唧/立牌/色纸/票谷/其他周边五类通过 typeIdx 控制)和价格输入。setDiscount 方法是议价功能的核心——接收一个折扣率参数(如 0.95 表示 95 折),从当前选中商品的原价计算折后价格,使用 Math.round 进行四舍五入,并将结果转为字符串赋值给 bargainPrice 驱动输入框更新。这种"一键折扣+手动输入"的双重回价机制,既满足了快速响应的场景,也保留了精确报价的灵活性。

十、订单 Tab——GOrderTab 组件与物流轨迹

订单 Tab 提供了订单筛选、详情查看、售后申请和物流轨迹展示。

ForEach(ORDER_ROWS, (o: OrderRow, idx: number) => {
  Column({ space: 10 }) {
    Row({ space: 6 }) {
      Text(o.seller)
        .fontSize(12)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1F2937')
        .layoutWeight(1)
      Text(o.status)
        .fontSize(11)
        .fontColor(o.statusColor)
    }
    .width('100%')

    Row({ space: 10 }) {
      Column()
        .width(4)
        .height(52)
        .borderRadius(2)
        .backgroundColor(gradeColor(o.grade))

      Column({ space: 4 }) {
        Text(o.goods)
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1F2937')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Text('订单号 ' + o.id)
          .fontSize(10)
          .fontColor('#94A3B8')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Column({ space: 3 }) {
        Text('¥' + o.price)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF6A00')
        Text('x' + o.num)
          .fontSize(10)
          .fontColor('#94A3B8')
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')

    Row({ space: 8 }) {
      Text(o.grade)
        .fontSize(9)
        .fontColor('#FFFFFF')
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
        .borderRadius(8)
        .backgroundColor(gradeColor(o.grade))
      Row()
        .layoutWeight(1)
      Text('查物流')
        .fontSize(11)
        .fontColor('#2563EB')
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .borderRadius(12)
        .border({ width: 1, color: '#BFDBFE' })
        .onClick(() => {
          this.detailIdx = idx
          this.showLog = true
        })
      Text('售后')
        .fontSize(11)
        .fontColor('#EF4444')
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .borderRadius(12)
        .border({ width: 1, color: '#FECACA' })
        .onClick(() => {
          this.detailIdx = idx
          this.reasonIdx = -1
          this.showAfter = true
        })
      Text('详情')
        .fontSize(11)
        .fontColor('#FFFFFF')
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .borderRadius(12)
        .backgroundColor('#FF6A00')
        .onClick(() => {
          this.detailIdx = idx
          this.showDetail = true
        })
    }
    .width('100%')
  }
  .width('100%')
  .padding(12)
  .borderRadius(14)
  .backgroundColor('#FFFFFF')
  .shadow({ radius: 4, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
}, (o: OrderRow) => o.id)

订单卡片的左侧使用了一个 4 像素宽的竖条,颜色由 gradeColor(o.grade) 决定,通过这种视觉编码让用户快速识别订单商品的稀有度等级。每张卡片底部提供"查物流"“售后”"详情"三个操作按钮,分别用不同颜色区分——物流用蓝色边框、售后用红色边框、详情用橙色实底。物流轨迹弹窗使用了时间线布局,每个节点包含一个彩色圆点和状态文字,已完成节点显示橙色(#FF6A00),运输中节点显示蓝色(#2563EB),待完成节点显示灰色——这种渐进式的颜色变化清晰表达了物流进度。售后申请弹窗提供了四个原因选项(商品与描述不符、运输破损、发错货/漏发、不想要了),通过 reasonIdx 管理选中状态,提交按钮在未选中原因时显示浅红色(#FCA5A5)不可用状态,选中后变为正常红色(#EF4444),实现了表单校验的视觉反馈。

十一、我的 Tab——GMineTab 组件与提现功能

我的 Tab 展示用户信息、钱包余额、消费账单、常逛店铺,以及提现、徽章墙和设置弹窗。

Row({ space: 8 }) {
  ForEach(WALLET_CARDS, (w: WalletCard) => {
    Column({ space: 5 }) {
      Text(w.title)
        .fontSize(11)
        .fontColor('#64748B')
      Text(w.value)
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1F2937')
      Text(w.desc)
        .fontSize(9)
        .fontColor('#FF6A00')
    }
    .layoutWeight(1)
    .padding({ top: 12, bottom: 12 })
    .borderRadius(12)
    .backgroundColor('#FFFFFF')
    .alignItems(HorizontalAlign.Center)
    .shadow({ radius: 4, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
  }, (w: WalletCard) => w.id.toString())
}
.width('100%')

钱包卡片区域使用 ForEach 渲染三张卡片——可用余额、冻结中和谷子积分,每张卡片独立带阴影效果。本月吃谷账单区域使用横向进度条展示了不同品类的消费占比——吧唧占 62% 使用橙色渐变、立牌占 48% 使用蓝色渐变、色纸占 30% 使用紫色渐变、票谷占 12% 使用绿色渐变,每条进度条宽度通过百分比直接控制。提现弹窗提供了微信和银行卡两种提现渠道选择,通过 channelIdx 管理选中状态,并配有快捷金额按钮(¥100/¥500/全部),"全部"按钮会将余额完整金额"1286.50"填入输入框。头部个人卡片使用了三色渐变背景(从 #FF6A00 经过 #FF8F3C#2563EB),将应用的双主色通过 140 度渐变完美融合。

十二、主页面 GPage 与底部导航

@Entry
@Component
struct GPage {
  @State activeTab: number = 0
  @State showSearch: boolean = false
  @State showMsg: boolean = false
  @State searchKw: string = ''

  build() {
    Column() {
      Column({ space: 0 }) {
        Row({ space: 10 }) {
          Text('谷子集市')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Row({ space: 6 }) {
            Text('🔍')
              .fontSize(13)
            Text('搜吧唧 / 立牌 / 色纸')
              .fontSize(11)
              .fontColor('#FDBA74')
              .layoutWeight(1)
          }
          .layoutWeight(1)
          .height(32)
          .padding({ left: 12, right: 12 })
          .borderRadius(16)
          .backgroundColor('rgba(255,255,255,0.22)')
          .onClick(() => {
            this.showSearch = true
          })

          Column() {
            Text('🔔')
              .fontSize(16)
            Column()
              .width(6)
              .height(6)
              .borderRadius(3)
              .backgroundColor('#FF3B30')
          }
          .onClick(() => {
            this.showMsg = true
          })
        }
        .width('100%')
        .padding({ left: 14, right: 14, top: 10, bottom: 10 })
      }
      .width('100%')
      .linearGradient({
        angle: 120,
        colors: [['#FF6A00', 0], ['#FF8F3C', 0.6], ['#2563EB', 1]]
      })

      Column() {
        if (this.activeTab === 0) {
          TodayTab()
        } else if (this.activeTab === 1) {
          MarketTab()
        } else if (this.activeTab === 2) {
          WantedTab()
        } else if (this.activeTab === 3) {
          SellTab()
        } else if (this.activeTab === 4) {
          GOrderTab()
        } else {
          GMineTab()
        }
      }
      .layoutWeight(1)
      .width('100%')

      Row({ space: 0 }) {
        ForEach(MAIN_TABS, (t: TabMeta, i: number) => {
          Column({ space: 3 }) {
            Text(t.icon)
              .fontSize(18)
            Text(t.name)
              .fontSize(10)
              .fontWeight(this.activeTab === i ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.activeTab === i ? '#FF6A00' : '#94A3B8')
            if (this.activeTab === i) {
              Column()
                .width(16)
                .height(3)
                .borderRadius(2)
                .backgroundColor('#FF6A00')
            }
          }
          .layoutWeight(1)
          .padding({ top: 8, bottom: 8 })
          .alignItems(HorizontalAlign.Center)
          .onClick(() => {
            this.activeTab = i
          })
        }, (t: TabMeta) => t.name)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F7FF')
  }
}

主页面 GPage 的头部导航栏使用了 120 度三段式渐变(橙到浅橙到靛蓝),搜索栏使用半透明白色背景并显示橙色占位文字。消息图标的红点指示器使用了 #FF3B30 的鲜红色,比应用主色更醒目。搜索弹窗提供了历史搜索和热门搜索两个维度——历史搜索使用 Flex 布局排列标签流,热门搜索使用带排名序号的列表形式,前三名分别用红色、橙色和琥珀色标注序号。底部导航栏使用 Emoji 图标(🥞📈📢📤📋👤)替代了传统的图标字体,选中状态使用橙色 #FF6A00 配合底部指示条,与主色保持一致。

整体业务流程图

0

1

2

3

4

5

抢购

盲盒机

闪购

6%

24%

70%

编辑

删除

上架

议价

改价

下架

查物流

售后

详情

提现

徽章

设置

应用启动 GPage

头部导航栏 三色渐变

activeTab 选择

TodayTab 今日谷

MarketTab 行情

WantedTab 求购

SellTab 出谷

GOrderTab 订单

GMineTab 我的

IP筛选条

商品瀑布流

操作选择

购买确认弹窗

抽谷弹窗 Math.random

晚场预告弹窗

随机概率

SSR 欧皇附体

SR 不错哦

R 再接再厉

谷子指数

7日双色柱状图

热门成交列表

点击收购

收购出价弹窗

发布求购

求购单列表

操作选择

编辑弹窗 editWanted

删除确认 removeWanted

发布弹窗 publishWanted

在售统计卡片

谷柜列表

操作选择

上架弹窗 applySell

议价弹窗 setDiscount

改价弹窗 applyEdit

下架弹窗 applyOff

订单筛选条

订单卡片列表

操作选择

物流轨迹时间线

售后申请弹窗

订单详情弹窗

个人信息卡

钱包三卡片

消费账单柱状图

操作选择

提现弹窗

徽章墙弹窗

设置弹窗

搜索弹窗

历史搜索标签流

热门搜索排行榜

技术点对比表格

技术维度今日谷Tab (TodayTab)行情Tab (MarketTab)求购Tab (WantedTab)出谷Tab (SellTab)订单Tab (GOrderTab)我的Tab (GMineTab)
核心数据结构GoodsItemMarketRow/PriceTrendWantedRowSellRowOrderRowWalletCard/BadgeWall
状态变量数量7个4个10个16个6个4个
弹窗数量3个(购买/盲盒/闪购)2个(收购/说明)3个(发布/编辑/删除)5个(上架/议价/改价/下架/攻略)3个(详情/售后/物流)3个(提现/徽章/设置)
CRUD操作完整CRUD完整CRUD
数据可视化商品瀑布流双色柱状图状态色彩编码物流时间线消费账单进度条
交互特色盲盒机随机抽取谷子指数行情不可变数组更新一键折扣议价售后原因校验三色渐变头部
列表布局Flex双列瀑布流Row等宽排列Column垂直列表Column垂直列表Column垂直列表ForEach卡片/菜单
渐变效果135度橙蓝渐变白色卡片白色卡片135度橙浅橙渐变白色卡片140度三色渐变

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 主题:活力橙 #FF6A00 × 靛蓝 #2563EB,底色 #F5F7FF,二次元撞色风
// 6 个底部 Tab:今日谷 / 行情 / 求购 / 出谷 / 订单 / 我的

interface IpChip {
  name: string
  count: number
}

interface GoodsItem {
  id: number
  name: string
  ip: string
  type: string
  grade: string
  price: number
  originPrice: number
  stock: number
  sold: number
  hot: boolean
  color: string
}

interface PriceTrend {
  day: string
  price: number
}

interface MarketRow {
  id: number
  name: string
  ip: string
  lastPrice: number
  change: number
  deals: number
  grade: string
}

interface WantedRow {
  id: number
  ip: string
  type: string
  budget: string
  note: string
  count: number
}

interface SellRow {
  id: number
  name: string
  grade: string
  price: number
  status: string
  statusColor: string
  asks: number
  views: number
}

interface OrderRow {
  id: string
  goods: string
  grade: string
  price: number
  num: number
  status: string
  statusColor: string
  seller: string
}

interface WalletCard {
  id: number
  title: string
  value: string
  desc: string
}

interface BadgeWall {
  id: number
  name: string
  lit: boolean
  color: string
}

interface TabMeta {
  name: string
  icon: string
}

interface MsgMeta {
  id: number
  title: string
  content: string
  time: string
  read: boolean
}

const IP_CHIPS: IpChip[] = [
  { name: '全部 IP', count: 1024 },
  { name: '时光代理人', count: 186 },
  { name: '咒术回战', count: 204 },
  { name: '排球少年', count: 168 },
  { name: '天官赐福', count: 122 },
  { name: '文豪野犬', count: 96 }
]

const TODAY_GOODS: GoodsItem[] = [
  { id: 1, name: '程小时 吧唧·雨夜', ip: '时光代理人', type: '吧唧', grade: 'SSR', price: 68, originPrice: 128, stock: 42, sold: 1682, hot: true, color: '#FFB88C' },
  { id: 2, name: '五条悟 Q版立牌', ip: '咒术回战', type: '立牌', grade: 'SSR', price: 88, originPrice: 158, stock: 26, sold: 2341, hot: true, color: '#A5C8F0' },
  { id: 3, name: '影山飞雄 拼图吧唧', ip: '排球少年', type: '吧唧', grade: 'SR', price: 35, originPrice: 68, stock: 88, sold: 3210, hot: false, color: '#BFD8A8' },
  { id: 4, name: '花城 色纸·血雨探花', ip: '天官赐福', type: '色纸', grade: 'SSR', price: 128, originPrice: 228, stock: 12, sold: 862, hot: true, color: '#E8A0A8' },
  { id: 5, name: '太宰治 亚克力钥匙扣', ip: '文豪野犬', type: '周边', grade: 'SR', price: 42, originPrice: 78, stock: 65, sold: 1207, hot: false, color: '#C5B3E8' },
  { id: 6, name: '陆光 镭射票根', ip: '时光代理人', type: '票谷', grade: 'R', price: 19, originPrice: 36, stock: 210, sold: 4560, hot: false, color: '#A8D8D8' }
]

const PRICE_TREND: PriceTrend[] = [
  { day: '8/18', price: 52 },
  { day: '8/19', price: 61 },
  { day: '8/20', price: 58 },
  { day: '8/21', price: 73 },
  { day: '8/22', price: 82 },
  { day: '8/23', price: 96 },
  { day: '8/24', price: 88 }
]

const MARKET_ROWS: MarketRow[] = [
  { id: 1, name: '程小时 吧唧·雨夜', ip: '时光代理人', lastPrice: 68, change: 12.5, deals: 326, grade: 'SSR' },
  { id: 2, name: '五条悟 Q版立牌', ip: '咒术回战', lastPrice: 88, change: 8.2, deals: 541, grade: 'SSR' },
  { id: 3, name: '花城 色纸·血雨探花', ip: '天官赐福', lastPrice: 128, change: -4.8, deals: 189, grade: 'SSR' },
  { id: 4, name: '影山飞雄 拼图吧唧', ip: '排球少年', lastPrice: 35, change: 3.1, deals: 762, grade: 'SR' },
  { id: 5, name: '太宰治 亚克力钥匙扣', ip: '文豪野犬', lastPrice: 42, change: -1.5, deals: 298, grade: 'SR' },
  { id: 6, name: '陆光 镭射票根', ip: '时光代理人', lastPrice: 19, change: 6.9, deals: 1103, grade: 'R' }
]

const GRADE_COLORS: string[] = ['#FF6A00', '#2563EB', '#8B5CF6']

function gradeColor(g: string): string {
  if (g === 'SSR') {
    return '#FF6A00'
  }
  if (g === 'SR') {
    return '#2563EB'
  }
  return '#8B5CF6'
}

function trendBarH(p: number): number {
  if (p >= 90) {
    return 88
  }
  if (p >= 75) {
    return 72
  }
  if (p >= 60) {
    return 56
  }
  if (p >= 45) {
    return 40
  }
  return 28
}

@Builder
function modalOverlay(onClose: () => void) {
  Column()
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(20,24,40,0.55)')
    .onClick(() => {
      onClose()
    })
}

// ==================== Tab 1 今日谷 ====================
@Component
export struct TodayTab {
  @State ipIdx: number = 0
  @State showBuy: boolean = false
  @State showLuck: boolean = false
  @State showFlash: boolean = false
  @State buyNum: number = 1
  @State selGoods: GoodsItem = TODAY_GOODS[0]
  @State luckyGrade: string = ''

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 10 }) {
          Row({ space: 10 }) {
            Column({ space: 3 }) {
              Text('今日吃谷日')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('每日 10:00 / 20:00 两波上新')
                .fontSize(11)
                .fontColor('#FFE3CF')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Text('盲盒机')
              .fontSize(12)
              .fontColor('#2563EB')
              .padding({ left: 12, right: 12, top: 7, bottom: 7 })
              .borderRadius(14)
              .backgroundColor('#FFFFFF')
              .onClick(() => {
                this.showLuck = true
              })
          }
          .width('100%')

          Row({ space: 8 }) {
            Column({ space: 2 }) {
              Text('16:42:08')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('晚场倒计时')
                .fontSize(9)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            .padding({ top: 8, bottom: 8 })
            .borderRadius(10)
            .backgroundColor('rgba(255,255,255,0.2)')
            .onClick(() => {
              this.showFlash = true
            })
            Column({ space: 2 }) {
              Text('2.6 万')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('今日吃谷人数')
                .fontSize(9)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('5 折')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('晚场专区')
                .fontSize(9)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            .onClick(() => {
              this.showFlash = true
            })
          }
          .width('100%')
        }
        .padding(14)
        .borderRadius(14)
        .linearGradient({
          angle: 135,
          colors: [['#FF6A00', 0.0], ['#2563EB', 1.0]]
        })
        .width('100%')

        Scroll() {
          Row({ space: 8 }) {
            ForEach(IP_CHIPS, (chip: IpChip) => {
              Text(chip.name + ' ' + chip.count)
                .fontSize(12)
                .fontColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#FFFFFF' : '#2563EB')
                .padding({ left: 13, right: 13, top: 7, bottom: 7 })
                .borderRadius(15)
                .backgroundColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#2563EB' : '#FFFFFF')
                .onClick(() => {
                  this.ipIdx = IP_CHIPS.indexOf(chip)
                })
            }, (chip: IpChip) => chip.name)
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .width('100%')

        Row() {
          Text('今日上新 6 款')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1F2937')
          Row()
            .layoutWeight(1)
          Text('真伪包赔 · 拆封可退')
            .fontSize(10)
            .fontColor('#94A3B8')
        }
        .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(TODAY_GOODS, (g: GoodsItem) => {
            Column({ space: 8 }) {
              Stack({  }) {
                Column()
                  .width('100%')
                  .height(120)
                  .borderRadius(10)
                  .backgroundColor(g.color)
                Text(g.grade)
                  .fontSize(10)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FFFFFF')
                  .padding({ left: 7, right: 7, top: 3, bottom: 3 })
                  .borderRadius({ topLeft: 10, bottomRight: 10 })
                  .backgroundColor(gradeColor(g.grade))
                if (g.hot) {
                  Text('HOT')
                    .fontSize(9)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FFFFFF')
                    .padding({ left: 6, right: 6, top: 3, bottom: 3 })
                    .borderRadius(4)
                    .backgroundColor('#EF4444')
                    .margin(6)
                }
              }
              .width('100%')

              Column({ space: 4 }) {
                Text(g.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#1F2937')
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(g.ip + ' · ' + g.type)
                  .fontSize(10)
                  .fontColor('#94A3B8')
                Row() {
                  Text('¥' + g.price)
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FF6A00')
                  Text('¥' + g.originPrice)
                    .fontSize(10)
                    .fontColor('#CBD5E1')
                    .decoration({ type: TextDecorationType.LineThrough })
                  Row()
                    .layoutWeight(1)
                  Text('抢')
                    .fontSize(12)
                    .fontColor('#FFFFFF')
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                    .borderRadius(12)
                    .backgroundColor('#FF6A00')
                    .onClick(() => {
                      this.selGoods = g
                      this.showBuy = true
                    })
                }
                .width('100%')
                Text('已售 ' + g.sold + ' · 库存 ' + g.stock)
                  .fontSize(9)
                  .fontColor('#94A3B8')
              }
              .alignItems(HorizontalAlign.Start)
              .width('100%')
            }
            .padding(10)
            .borderRadius(12)
            .backgroundColor('#FFFFFF')
            .width('48.5%')
            .margin({ bottom: 10 })
          }, (g: GoodsItem) => g.id.toString())
        }
        .width('100%')

        if (this.showBuy) {
          modalOverlay(() => {
            this.showBuy = false
          })
          Column({ space: 14 }) {
            Row() {
              Text('购买确认')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1F2937')
              Row()
                .layoutWeight(1)
              Text('×')
                .fontSize(18)
                .fontColor('#94A3B8')
                .onClick(() => {
                  this.showBuy = false
                })
            }
            .width('100%')

            Row({ space: 10 }) {
              Column()
                .width(64)
                .height(80)
                .borderRadius(10)
                .backgroundColor(this.selGoods.color)
              Column({ space: 4 }) {
                Text(this.selGoods.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#1F2937')
                Text(this.selGoods.ip + ' · ' + this.selGoods.grade + ' 级')
                  .fontSize(11)
                  .fontColor('#94A3B8')
                Text('每单限购 3 件 · 顺丰包邮')
                  .fontSize(10)
                  .fontColor('#2563EB')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')

            Row() {
              Text('数量')
                .fontSize(13)
                .fontColor('#475569')
              Row()
                .layoutWeight(1)
              Row({ space: 0 }) {
                Text('−')
                  .fontSize(16)
                  .fontColor(this.buyNum <= 1 ? '#E2E8F0' : '#FF6A00')
                  .padding(8)
                  .onClick(() => {
                    if (this.buyNum > 1) {
                      this.buyNum--
                    }
                  })
                Text(this.buyNum.toString())
                  .fontSize(13)
                  .fontColor('#1F2937')
                  .padding({ left: 12, right: 12 })
                Text('+')
                  .fontSize(16)
                  .fontColor(this.buyNum >= 3 ? '#E2E8F0' : '#FF6A00')
                  .padding(8)
                  .onClick(() => {
                    if (this.buyNum < 3) {
                      this.buyNum++
                    }
                  })
              }
              .borderRadius(8)
              .backgroundColor('#F1F5F9')
            }
            .width('100%')

            Row() {
              Column({ space: 1 }) {
                Text('合计')
                  .fontSize(11)
                  .fontColor('#94A3B8')
                Text('¥' + (this.selGoods.price * this.buyNum))
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF6A00')
              }
              .alignItems(HorizontalAlign.Start)

              Row()
                .layoutWeight(1)

              Text('立即支付')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 22, right: 22, top: 9, bottom: 9 })
                .borderRadius(18)
                .linearGradient({
                  angle: 90,
                  colors: [['#FF6A00', 0.0], ['#FF9A4D', 1.0]]
                })
                .onClick(() => {
                  this.showBuy = false
                })
            }
            .width('100%')
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('92%')
        }

        if (this.showLuck) {
          modalOverlay(() => {
            this.showLuck = false
          })
          Column({ space: 14 }) {
            Text('谷子盲盒机')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            Text('¥39/抽 · SSR 概率 6%')
              .fontSize(12)
              .fontColor('#94A3B8')
            Row({ space: 12 }) {
              Column({ space: 5 }) {
                Column()
                  .width(76)
                  .height(76)
                  .borderRadius(12)
                  .backgroundColor(this.luckyGrade === '' ? '#FDBA74' : '#FF6A00')
                Text(this.luckyGrade === '' ? '?' : this.luckyGrade)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF6A00')
              }
              .alignItems(HorizontalAlign.Center)
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)

            Text(this.luckyGrade === 'SSR' ? '欧皇附体!抽中 SSR 吧唧!' : (this.luckyGrade === 'SR' ? '不错哦,SR 入手!' : (this.luckyGrade === 'R' ? '再接再厉~' : '点击下方按钮开始抽')))
              .fontSize(12)
              .fontColor('#2563EB')

            Row({ space: 10 }) {
              Text('抽一次 ¥39')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 9, bottom: 9 })
                .borderRadius(18)
                .backgroundColor('#2563EB')
                .onClick(() => {
                  const r: number = Math.random()
                  if (r < 0.06) {
                    this.luckyGrade = 'SSR'
                  } else if (r < 0.3) {
                    this.luckyGrade = 'SR'
                  } else {
                    this.luckyGrade = 'R'
                  }
                })
              Text('收手')
                .fontSize(13)
                .fontColor('#FF6A00')
                .padding({ left: 20, right: 20, top: 9, bottom: 9 })
                .borderRadius(18)
                .backgroundColor('#FFF3E8')
                .onClick(() => {
                  this.showLuck = false
                })
            }
          }
          .padding(20)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .alignItems(HorizontalAlign.Center)
          .width('84%')
        }

        if (this.showFlash) {
          modalOverlay(() => {
            this.showFlash = false
          })
          Column({ space: 10 }) {
            Text('晚场 20:00 闪购预告')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            ForEach(['夏油杰 吧唧·白衣 · 5 折 ¥59', '谢怜 色纸·花城 ×2 · 7 折 ¥96', '中原中也 立牌·Q 版 · 6 折 ¥52'], (t: string) => {
              Text('· ' + t)
                .fontSize(12)
                .fontColor('#475569')
                .width('100%')
            }, (t: string) => t)
            Text('开抢提醒已开启')
              .fontSize(11)
              .fontColor('#2563EB')
            Text('好的')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .padding({ left: 26, right: 26, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor('#FF6A00')
              .onClick(() => {
                this.showFlash = false
              })
          }
          .padding(20)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('84%')
        }
      }
      .padding(12)
    }
    .backgroundColor('#F5F7FF')
    .layoutWeight(1)
  }
}

// ==================== Tab 2 行情 ====================
@Component
export struct MarketTab {
  @State ipIdx: number = 0
  @State showDeal: boolean = false
  @State showTip: boolean = false
  @State selRow: MarketRow = MARKET_ROWS[0]

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 12 }) {
          Row() {
            Text('谷子指数 1186.4')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            Text('+2.8%')
              .fontSize(11)
              .fontColor('#FFFFFF')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .borderRadius(4)
              .backgroundColor('#EF4444')
            Row()
              .layoutWeight(1)
            Text('7 日')
              .fontSize(10)
              .fontColor('#94A3B8')
          }
          .width('100%')

          Row({ space: 12 }) {
            ForEach(PRICE_TREND, (t: PriceTrend) => {
              Column({ space: 4 }) {
                Text('¥' + t.price)
                  .fontSize(8)
                  .fontColor(t.price >= 85 ? '#FF6A00' : '#94A3B8')
                Column()
                  .width(20)
                  .height(trendBarH(t.price))
                  .borderRadius({ topLeft: 4, topRight: 4 })
                  .linearGradient({
                    angle: 180,
                    colors: t.price >= 85
                      ? [['#FF9A4D', 0.0], ['#FF6A00', 1.0]]
                      : [['#93C5FD', 0.0], ['#2563EB', 1.0]]
                  })
                Text(t.day)
                  .fontSize(8)
                  .fontColor('#94A3B8')
              }
              .alignItems(HorizontalAlign.Center)
            }, (t: PriceTrend) => t.day)
          }
          .width('100%')
          .padding({ top: 4 })
        }
        .padding(12)
        .borderRadius(14)
        .backgroundColor('#FFFFFF')
        .width('100%')

        Row({ space: 8 }) {
          ForEach(IP_CHIPS, (chip: IpChip) => {
            Text(chip.name)
              .fontSize(11)
              .fontColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#FFFFFF' : '#475569')
              .padding({ left: 11, right: 11, top: 6, bottom: 6 })
              .borderRadius(13)
              .backgroundColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#FF6A00' : '#FFFFFF')
              .onClick(() => {
                this.ipIdx = IP_CHIPS.indexOf(chip)
              })
          }, (chip: IpChip) => chip.name)
        }
        .width('100%')

        Row() {
          Text('热门成交')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1F2937')
          Row()
            .layoutWeight(1)
          Text('行情说明')
            .fontSize(11)
            .fontColor('#2563EB')
            .onClick(() => {
              this.showTip = true
            })
        }
        .width('100%')

        ForEach(MARKET_ROWS, (row: MarketRow) => {
          Row({ space: 12 }) {
            Column()
              .width(52)
              .height(52)
              .borderRadius(10)
              .backgroundColor(gradeColor(row.grade))

            Column({ space: 4 }) {
              Row({ space: 6 }) {
                Text(row.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#1F2937')
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(row.grade)
                  .fontSize(9)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .borderRadius(3)
                  .backgroundColor(gradeColor(row.grade))
              }
              Text(row.ip)
                .fontSize(11)
                .fontColor('#94A3B8')
              Text('近 7 日成交 ' + row.deals + ' 笔')
                .fontSize(10)
                .fontColor('#94A3B8')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Column({ space: 4 }) {
              Text('¥' + row.lastPrice)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(row.change > 0 ? '#EF4444' : '#16A34A')
              Text((row.change > 0 ? '↑ +' : '↓ ') + row.change + '%')
                .fontSize(11)
                .fontColor(row.change > 0 ? '#EF4444' : '#16A34A')
              Text('收购')
                .fontSize(10)
                .fontColor('#2563EB')
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .borderRadius(9)
                .backgroundColor('#EAF1FE')
                .onClick(() => {
                  this.selRow = row
                  this.showDeal = true
                })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFFFFF')
        }, (row: MarketRow) => row.id.toString())

        if (this.showDeal) {
          modalOverlay(() => {
            this.showDeal = false
          })
          Column({ space: 12 }) {
            Text('收购出价')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')
            Text('「' + this.selRow.name + '」当前 ¥' + this.selRow.lastPrice)
              .fontSize(12)
              .fontColor('#475569')
              .width('100%')
            Row({ space: 8 }) {
              ForEach(['¥' + this.selRow.lastPrice, '+5%', '+10%', 'Custom'], (b: string) => {
                Text(b)
                  .fontSize(12)
                  .fontColor('#2563EB')
                  .padding({ left: 12, right: 12, top: 7, bottom: 7 })
                  .borderRadius(10)
                  .backgroundColor('#EAF1FE')
              }, (b: string) => b)
            }
            .width('100%')
            Text('· 收购单 7 天内有效,卖家接单即锁定\n· 平台担保交易,假一赔三')
              .fontSize(11)
              .fontColor('#94A3B8')
              .lineHeight(18)
              .width('100%')
            Text('发布收购')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .padding({ left: 0, right: 0, top: 9, bottom: 9 })
              .borderRadius(18)
              .linearGradient({
                angle: 90,
                colors: [['#2563EB', 0.0], ['#FF6A00', 1.0]]
              })
              .width('100%')
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showDeal = false
              })
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('88%')
        }

        if (this.showTip) {
          modalOverlay(() => {
            this.showTip = false
          })
          Column({ space: 10 }) {
            Text('行情说明')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            Text('1. 行情价 = 近 7 日平台真实成交均价\n2. 涨跌幅对比昨日行情价\n3. 成交量仅统计担保交易订单\n4. 恶意炒价订单会被剔除')
              .fontSize(12)
              .fontColor('#475569')
              .lineHeight(20)
            Text('知道了')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .padding({ left: 26, right: 26, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor('#2563EB')
              .onClick(() => {
                this.showTip = false
              })
          }
          .padding(20)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('84%')
        }
      }
      .padding(12)
    }
    .backgroundColor('#F5F7FF')
    .layoutWeight(1)
  }
}

// ==================== Tab 3 求购 ====================
@Component
export struct WantedTab {
  @State showPub: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State editIdx: number = -1
  @State delIdx: number = -1
  @State inputIp: string = ''
  @State inputType: string = '吧唧'
  @State inputBudget: string = ''
  @State inputNote: string = ''
  @State typeIdx: number = 0
  @State wantedList: WantedRow[] = [
    { id: 1, ip: '时光代理人', type: '吧唧', budget: '¥50-80', note: '要程小时雨夜,品相 9 成新以上', count: 2 },
    { id: 2, ip: '天官赐福', type: '色纸', budget: '¥100-150', note: '花城血雨探花,可小刀', count: 1 },
    { id: 3, ip: '排球少年', type: '立牌', budget: '¥30-60', note: '影山/及川都可,量大收', count: 4 }
  ]

  publishWanted() {
    const r: WantedRow[] = []
    for (let i = 0; i < this.wantedList.length; i++) {
      r.push(this.wantedList[i])
    }
    r.push({
      id: this.wantedList.length + 1,
      ip: this.inputIp === '' ? '咒术回战' : this.inputIp,
      type: this.inputType,
      budget: this.inputBudget === '' ? '¥40-100' : this.inputBudget,
      note: this.inputNote === '' ? '长期有效,价优可秒' : this.inputNote,
      count: 1
    })
    this.wantedList = r
    this.showPub = false
  }

  editWanted() {
    const r: WantedRow[] = []
    for (let i = 0; i < this.wantedList.length; i++) {
      if (i === this.editIdx) {
        r.push({
          id: this.wantedList[i].id,
          ip: this.inputIp === '' ? this.wantedList[i].ip : this.inputIp,
          type: this.inputType === '' ? this.wantedList[i].type : this.inputType,
          budget: this.inputBudget === '' ? this.wantedList[i].budget : this.inputBudget,
          note: this.inputNote === '' ? this.wantedList[i].note : this.inputNote,
          count: this.wantedList[i].count
        })
      } else {
        r.push(this.wantedList[i])
      }
    }
    this.wantedList = r
    this.showEdit = false
  }

  removeWanted() {
    const r: WantedRow[] = []
    for (let i = 0; i < this.wantedList.length; i++) {
      if (i !== this.delIdx) {
        r.push(this.wantedList[i])
      }
    }
    this.wantedList = r
    this.showDel = false
  }

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 10 }) {
          Column({ space: 3 }) {
            Text('我的求购')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            Text('发布求购让卖家找上门')
              .fontSize(10)
              .fontColor('#94A3B8')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('发布求购')
            .fontSize(12)
            .fontColor('#FFFFFF')
            .padding({ left: 14, right: 14, top: 7, bottom: 7 })
            .borderRadius(14)
            .backgroundColor('#FF6A00')
            .onClick(() => {
              this.inputIp = ''
              this.inputType = '吧唧'
              this.inputBudget = ''
              this.inputNote = ''
              this.showPub = true
            })
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')

        ForEach(this.wantedList, (w: WantedRow, idx: number) => {
          Column({ space: 10 }) {
            Row({ space: 8 }) {
              Text(w.ip)
                .fontSize(11)
                .fontColor('#FFFFFF')
                .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                .borderRadius(6)
                .backgroundColor('#2563EB')
              Text(w.type)
                .fontSize(11)
                .fontColor('#FF6A00')
                .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                .borderRadius(6)
                .backgroundColor('#FFF3E8')
              Row()
                .layoutWeight(1)
              Text('求 ' + w.count + ' 件')
                .fontSize(11)
                .fontColor('#94A3B8')
            }
            .width('100%')

            Text(w.note)
              .fontSize(12)
              .fontColor('#475569')
              .width('100%')

            Row() {
              Text('预算 ' + w.budget)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF6A00')
              Row()
                .layoutWeight(1)
              Text('编辑')
                .fontSize(11)
                .fontColor('#2563EB')
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .borderRadius(10)
                .border({ width: 1, color: '#2563EB' })
                .onClick(() => {
                  this.editIdx = idx
                  this.inputIp = w.ip
                  this.inputType = w.type
                  this.inputBudget = w.budget
                  this.inputNote = w.note
                  this.showEdit = true
                })
              Text('删除')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .borderRadius(10)
                .backgroundColor('#EF4444')
                .onClick(() => {
                  this.delIdx = idx
                  this.showDel = true
                })
            }
            .width('100%')
          }
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFFFFF')
        }, (w: WantedRow) => ('w' + w.id).toString())

        Text('· 求购单 30 天有效,可随时关闭\n· 有卖家接单会第一时间推送')
          .fontSize(10)
          .fontColor('#94A3B8')
          .lineHeight(16)
          .width('100%')

        if (this.showPub) {
          modalOverlay(() => {
            this.showPub = false
          })
          Column({ space: 12 }) {
            Text('发布求购')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            TextInput({ placeholder: 'IP 名称(如:时光代理人)', text: this.inputIp })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputIp = v
              })
              .width('100%')

            Column({ space: 8 }) {
              Text('谷子类型')
                .fontSize(12)
                .fontColor('#94A3B8')
                .width('100%')
              Row({ space: 8 }) {
                ForEach(['吧唧', '立牌', '色纸', '票谷', '其他'], (t: string) => {
                  Text(t)
                    .fontSize(12)
                    .fontColor(this.typeIdx === ['吧唧', '立牌', '色纸', '票谷', '其他'].indexOf(t) ? '#FFFFFF' : '#475569')
                    .padding({ left: 13, right: 13, top: 6, bottom: 6 })
                    .borderRadius(10)
                    .backgroundColor(this.typeIdx === ['吧唧', '立牌', '色纸', '票谷', '其他'].indexOf(t) ? '#FF6A00' : '#F1F5F9')
                    .onClick(() => {
                      this.typeIdx = ['吧唧', '立牌', '色纸', '票谷', '其他'].indexOf(t)
                      this.inputType = t
                    })
                }, (t: string) => t)
              }
            }
            .width('100%')

            TextInput({ placeholder: '预算区间(如:¥50-80)', text: this.inputBudget })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputBudget = v
              })
              .width('100%')

            TextInput({ placeholder: '补充说明(品相/角色要求)', text: this.inputNote })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputNote = v
              })
              .width('100%')

            Text('发布')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .padding({ left: 0, right: 0, top: 9, bottom: 9 })
              .borderRadius(18)
              .backgroundColor('#FF6A00')
              .width('100%')
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.publishWanted()
              })
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .constraintSize({ maxHeight: '80%' })
          .width('92%')
        }

        if (this.showEdit) {
          modalOverlay(() => {
            this.showEdit = false
          })
          Column({ space: 12 }) {
            Text('编辑求购')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            TextInput({ placeholder: 'IP 名称', text: this.inputIp })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputIp = v
              })
              .width('100%')

            TextInput({ placeholder: '预算区间', text: this.inputBudget })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputBudget = v
              })
              .width('100%')

            TextInput({ placeholder: '补充说明', text: this.inputNote })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputNote = v
              })
              .width('100%')

            Row({ space: 10 }) {
              Text('取消')
                .fontSize(13)
                .fontColor('#475569')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showEdit = false
                })
              Text('保存')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#2563EB')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.editWanted()
                })
            }
            .width('100%')
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .constraintSize({ maxHeight: '80%' })
          .width('92%')
        }

        if (this.showDel) {
          modalOverlay(() => {
            this.showDel = false
          })
          Column({ space: 12 }) {
            Text('删除求购单')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
            Text('删除后已接单的卖家将收到通知,确认删除?')
              .fontSize(12)
              .fontColor('#94A3B8')
              .textAlign(TextAlign.Center)
            Row({ space: 10 }) {
              Text('取消')
                .fontSize(13)
                .fontColor('#475569')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showDel = false
                })
              Text('删除')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#EF4444')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.removeWanted()
                })
            }
            .width('100%')
          }
          .padding(18)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('84%')
        }
      }
      .padding(12)
    }
    .backgroundColor('#F5F7FF')
    .layoutWeight(1)
  }
}

// ==================== Tab 4 出谷 ====================
@Component
export struct SellTab {
  @State sellList: SellRow[] = [
    { id: 1, name: '程小时 吧唧·雨夜', grade: 'SSR', price: 68, status: '寄售中', statusColor: '#FF6A00', asks: 12, views: 863 },
    { id: 2, name: '五条悟 Q版立牌', grade: 'SSR', price: 88, status: '寄售中', statusColor: '#FF6A00', asks: 23, views: 1521 },
    { id: 3, name: '影山飞雄 拼图吧唧', grade: 'SR', price: 35, status: '已预定', statusColor: '#2563EB', asks: 5, views: 402 },
    { id: 4, name: '花城 色纸·血雨探花', grade: 'SSR', price: 128, status: '已下架', statusColor: '#9CA3AF', asks: 0, views: 98 },
    { id: 5, name: '太宰治 亚克力钥匙扣', grade: 'SR', price: 42, status: '已售出', statusColor: '#16A34A', asks: 0, views: 655 },
    { id: 6, name: '陆光 镭射票根', grade: 'R', price: 19, status: '寄售中', statusColor: '#FF6A00', asks: 8, views: 231 }
  ]
  @State showSell: boolean = false
  @State showBargain: boolean = false
  @State showOff: boolean = false
  @State showEdit: boolean = false
  @State showAsk: boolean = false
  @State bargainIdx: number = -1
  @State offIdx: number = -1
  @State editIdx: number = -1
  @State inputName: string = ''
  @State inputPrice: string = ''
  @State editPrice: string = ''
  @State inputGrade: string = 'SSR'
  @State gradeIdx: number = 0
  @State typeIdx: number = 0
  @State sellType: string = '吧唧'
  @State bargainPrice: string = ''

  applySell() {
    const r: SellRow[] = []
    for (let i = 0; i < this.sellList.length; i++) {
      r.push(this.sellList[i])
    }
    r.unshift({
      id: this.sellList.length + 1,
      name: this.inputName === '' ? '未命名谷子' : this.inputName,
      grade: this.inputGrade,
      price: Number(this.inputPrice === '' ? '0' : this.inputPrice),
      status: '寄售中',
      statusColor: '#FF6A00',
      asks: 0,
      views: 0
    })
    this.sellList = r
    this.showSell = false
    this.inputName = ''
    this.inputPrice = ''
  }

  applyOff() {
    const r: SellRow[] = []
    for (let i = 0; i < this.sellList.length; i++) {
      if (i === this.offIdx) {
        r.push({
          id: this.sellList[i].id,
          name: this.sellList[i].name,
          grade: this.sellList[i].grade,
          price: this.sellList[i].price,
          status: '已下架',
          statusColor: '#9CA3AF',
          asks: this.sellList[i].asks,
          views: this.sellList[i].views
        })
      } else {
        r.push(this.sellList[i])
      }
    }
    this.sellList = r
    this.showOff = false
  }

  applyEdit() {
    const r: SellRow[] = []
    for (let i = 0; i < this.sellList.length; i++) {
      if (i === this.editIdx) {
        r.push({
          id: this.sellList[i].id,
          name: this.sellList[i].name,
          grade: this.sellList[i].grade,
          price: Number(this.editPrice === '' ? '0' : this.editPrice),
          status: this.sellList[i].status,
          statusColor: this.sellList[i].statusColor,
          asks: this.sellList[i].asks,
          views: this.sellList[i].views
        })
      } else {
        r.push(this.sellList[i])
      }
    }
    this.sellList = r
    this.showEdit = false
  }

  setDiscount(rate: number) {
    const p: number = this.sellList[this.bargainIdx].price
    this.bargainPrice = String(Math.round(p * rate))
  }

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 10 }) {
          Row({ space: 0 }) {
            Column({ space: 3 }) {
              Text('4')
                .fontSize(22)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('在售中')
                .fontSize(10)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)

            Column({ space: 3 }) {
              Text('1')
                .fontSize(22)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('已售出')
                .fontSize(10)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)

            Column({ space: 3 }) {
              Text('3870')
                .fontSize(22)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('总浏览')
                .fontSize(10)
                .fontColor('#FFE3CF')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }
          .width('100%')

          Row({ space: 8 }) {
            Text('+ 上架好谷')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF6A00')
              .padding({ left: 18, right: 18, top: 9, bottom: 9 })
              .borderRadius(18)
              .backgroundColor('#FFFFFF')
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showSell = true
              })

            Text('出谷攻略')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 18, right: 18, top: 9, bottom: 9 })
              .borderRadius(18)
              .backgroundColor('rgba(255,255,255,0.25)')
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showAsk = true
              })
          }
          .width('100%')
        }
        .padding(14)
        .borderRadius(16)
        .linearGradient({
          angle: 135,
          colors: [['#FF6A00', 0], ['#FF9A44', 1]]
        })
        .shadow({ radius: 12, color: 'rgba(255,106,0,0.28)', offsetX: 0, offsetY: 5 })
        .width('100%')

        Row({ space: 6 }) {
          Text('我的谷柜')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1F2937')
            .layoutWeight(1)

          Text('共 ' + this.sellList.length + ' 件')
            .fontSize(11)
            .fontColor('#6B7280')
        }
        .width('100%')
        .padding({ left: 4, right: 4 })

        ForEach(this.sellList, (s: SellRow, idx: number) => {
          Row({ space: 12 }) {
            Column({ space: 4 }) {
              Text(s.grade)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text(s.price + '元')
                .fontSize(10)
                .fontColor('#FFF3E8')
            }
            .width(58)
            .padding({ top: 12, bottom: 12 })
            .borderRadius(12)
            .backgroundColor(gradeColor(s.grade))
            .alignItems(HorizontalAlign.Center)
            .shadow({ radius: 6, color: 'rgba(37,99,235,0.18)', offsetX: 0, offsetY: 3 })

            Column({ space: 5 }) {
              Row({ space: 6 }) {
                Text(s.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#1F2937')
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .layoutWeight(1)

                Text(s.status)
                  .fontSize(9)
                  .fontColor(s.statusColor)
                  .padding({ left: 7, right: 7, top: 2, bottom: 2 })
                  .borderRadius(8)
                  .backgroundColor('#F5F7FF')
              }
              .width('100%')

              Row({ space: 10 }) {
                Text('浏览 ' + s.views)
                  .fontSize(10)
                  .fontColor('#9CA3AF')
                Text('议价 ' + s.asks + ' 条')
                  .fontSize(10)
                  .fontColor('#FF6A00')
              }
              .width('100%')

              Row({ space: 6 }) {
                Text('议价')
                  .fontSize(10)
                  .fontColor('#2563EB')
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .borderRadius(10)
                  .border({ width: 1, color: '#BFDBFE' })
                  .onClick(() => {
                    this.bargainIdx = idx
                    this.bargainPrice = ''
                    this.showBargain = true
                  })

                Text('改价')
                  .fontSize(10)
                  .fontColor('#FF6A00')
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .borderRadius(10)
                  .border({ width: 1, color: '#FED7AA' })
                  .onClick(() => {
                    this.editIdx = idx
                    this.editPrice = String(s.price)
                    this.showEdit = true
                  })

                if (s.status === '寄售中' || s.status === '已预定') {
                  Text('下架')
                    .fontSize(10)
                    .fontColor('#EF4444')
                    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                    .borderRadius(10)
                    .border({ width: 1, color:'#FECACA' })
                    .onClick(() => {
                      this.offIdx = idx
                      this.showOff = true
                    })
                }
              }
              .width('100%')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor(idx % 2 === 0 ? '#FFFFFF' : '#F0F5FF')
          .shadow({ radius: 5, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
        }, (s: SellRow) => s.id.toString())

        Column({ space: 8 }) {
          Row({ space: 6 }) {
            Text('寄售小贴士')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#2563EB')
            Text('· 发货前请拍摄打包视频')
              .fontSize(10)
              .fontColor('#6B7280')
              .layoutWeight(1)
          }
          .width('100%')

          Row({ space: 6 }) {
            Text('审核时效')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF6A00')
            Text('· 工作日 2 小时内完成审核')
              .fontSize(10)
              .fontColor('#6B7280')
              .layoutWeight(1)
          }
          .width('100%')
        }
        .width('100%')
        .padding(14)
        .borderRadius(14)
        .backgroundColor('#FFFFFF')
        .border({ width: 1, color: '#E5E7EB' })

        if (this.showSell) {
          modalOverlay(() => {
            this.showSell = false
          })
          Column({ space: 12 }) {
            Text('上架好谷')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            TextInput({ placeholder: '谷子名称(如:程小时 吧唧·雨夜)', text: this.inputName })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputName = v
              })

            Column({ space: 8 }) {
              Text('档位')
                .fontSize(12)
                .fontColor('#475569')
                .width('100%')

              Row({ space: 8 }) {
                Text('SSR')
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.gradeIdx === 0 ? '#FFFFFF' : '#FF6A00')
                  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(this.gradeIdx === 0 ? '#FF6A00' : '#FFF3E8')
                  .onClick(() => {
                    this.gradeIdx = 0
                    this.inputGrade = 'SSR'
                  })

                Text('SR')
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.gradeIdx === 1 ? '#FFFFFF' : '#2563EB')
                  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(this.gradeIdx === 1 ? '#2563EB' : '#EFF4FF')
                  .onClick(() => {
                    this.gradeIdx = 1
                    this.inputGrade = 'SR'
                  })

                Text('R')
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.gradeIdx === 2 ? '#FFFFFF' : '#8B5CF6')
                  .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(this.gradeIdx === 2 ? '#8B5CF6' : '#F5F0FF')
                  .onClick(() => {
                    this.gradeIdx = 2
                    this.inputGrade = 'R'
                  })
              }
              .width('100%')
            }
            .width('100%')

            Column({ space: 8 }) {
              Text('品类')
                .fontSize(12)
                .fontColor('#475569')
                .width('100%')

              Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
                Text('吧唧')
                  .fontSize(11)
                  .fontColor(this.typeIdx === 0 ? '#FFFFFF' : '#475569')
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                  .borderRadius(10)
                  .backgroundColor(this.typeIdx === 0 ? '#FF6A00' : '#F1F5F9')
                  .margin(4)
                  .onClick(() => {
                    this.typeIdx = 0
                    this.sellType = '吧唧'
                  })

                Text('立牌')
                  .fontSize(11)
                  .fontColor(this.typeIdx === 1 ? '#FFFFFF' : '#475569')
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                  .borderRadius(10)
                  .backgroundColor(this.typeIdx === 1 ? '#FF6A00' : '#F1F5F9')
                  .margin(4)
                  .onClick(() => {
                    this.typeIdx = 1
                    this.sellType = '立牌'
                  })

                Text('色纸')
                  .fontSize(11)
                  .fontColor(this.typeIdx === 2 ? '#FFFFFF' : '#475569')
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                  .borderRadius(10)
                  .backgroundColor(this.typeIdx === 2 ? '#FF6A00' : '#F1F5F9')
                  .margin(4)
                  .onClick(() => {
                    this.typeIdx = 2
                    this.sellType = '色纸'
                  })

                Text('票谷')
                  .fontSize(11)
                  .fontColor(this.typeIdx === 3 ? '#FFFFFF' : '#475569')
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                  .borderRadius(10)
                  .backgroundColor(this.typeIdx === 3 ? '#FF6A00' : '#F1F5F9')
                  .margin(4)
                  .onClick(() => {
                    this.typeIdx = 3
                    this.sellType = '票谷'
                  })

                Text('其他周边')
                  .fontSize(11)
                  .fontColor(this.typeIdx === 4 ? '#FFFFFF' : '#475569')
                  .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                  .borderRadius(10)
                  .backgroundColor(this.typeIdx === 4 ? '#FF6A00' : '#F1F5F9')
                  .margin(4)
                  .onClick(() => {
                    this.typeIdx = 4
                    this.sellType = '其他周边'
                  })
              }
              .width('100%')
            }
            .width('100%')

            TextInput({ placeholder: '心理价(元)', text: this.inputPrice })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.inputPrice = v
              })

            Row({ space: 10 }) {
              Text('取消')
                .fontSize(13)
                .fontColor('#64748B')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showSell = false
                })

              Text('确认上架')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .linearGradient({
                  angle: 90,
                  colors: [['#FF6A00', 0.0], ['#FF9A4D', 1.0]]
                })
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.applySell()
                })
            }
            .width('100%')
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('92%')
          .constraintSize({ maxHeight: '80%' })
        }

        if (this.showBargain) {
          modalOverlay(() => {
            this.showBargain = false
          })
          Column({ space: 12 }) {
            Text('收到议价')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            Column({ space: 4 }) {
              Text(this.sellList[this.bargainIdx].name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1F2937')
              Text('当前挂价 ¥' + this.sellList[this.bargainIdx].price + ' · 已收到 ' + this.sellList[this.bargainIdx].asks + ' 条议价')
                .fontSize(11)
                .fontColor('#94A3B8')
            }
            .alignItems(HorizontalAlign.Start)
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#F0F5FF')

            Text('一键回价')
              .fontSize(12)
              .fontColor('#475569')
              .width('100%')

            Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
              Text('95 折')
                .fontSize(12)
                .fontColor('#FF6A00')
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .borderRadius(12)
                .backgroundColor('#FFF3E8')
                .margin(4)
                .onClick(() => {
                  this.setDiscount(0.95)
                })

              Text('9 折')
                .fontSize(12)
                .fontColor('#FF6A00')
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .borderRadius(12)
                .backgroundColor('#FFF3E8')
                .margin(4)
                .onClick(() => {
                  this.setDiscount(0.9)
                })

              Text('85 折')
                .fontSize(12)
                .fontColor('#2563EB')
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .borderRadius(12)
                .backgroundColor('#EFF4FF')
                .margin(4)
                .onClick(() => {
                  this.setDiscount(0.85)
                })

              Text('8 折')
                .fontSize(12)
                .fontColor('#2563EB')
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .borderRadius(12)
                .backgroundColor('#EFF4FF')
                .margin(4)
                .onClick(() => {
                  this.setDiscount(0.8)
                })
            }
            .width('100%')

            TextInput({ placeholder: '手动输入回价(元)', text: this.bargainPrice })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.bargainPrice = v
              })

            Row({ space: 10 }) {
              Text('暂不议价')
                .fontSize(13)
                .fontColor('#64748B')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showBargain = false
                })

              Text('同意 ' + (this.bargainPrice === '' ? '议价' : '¥' + this.bargainPrice))
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#2563EB')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showBargain = false
                })
            }
            .width('100%')
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('92%')
          .constraintSize({ maxHeight: '80%' })
        }

        if (this.showEdit) {
          modalOverlay(() => {
            this.showEdit = false
          })
          Column({ space: 12 }) {
            Text('修改挂价')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            Text(this.sellList[this.editIdx].name)
              .fontSize(12)
              .fontColor('#64748B')
              .width('100%')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            TextInput({ placeholder: '新价格(元)', text: this.editPrice })
              .fontSize(12)
              .height(38)
              .borderRadius(10)
              .backgroundColor('#F1F5F9')
              .onChange((v: string) => {
                this.editPrice = v
              })

            Text('提示:降价会重新推送给关注买家')
              .fontSize(10)
              .fontColor('#94A3B8')
              .width('100%')

            Row({ space: 10 }) {
              Text('取消')
                .fontSize(13)
                .fontColor('#64748B')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showEdit = false
                })

              Text('确认改价')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#FF6A00')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.applyEdit()
                })
            }
            .width('100%')
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('88%')
        }

        if (this.showOff) {
          modalOverlay(() => {
            this.showOff = false
          })
          Column({ space: 14 }) {
            Text('确认下架?')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#EF4444')
              .width('100%')
              .textAlign(TextAlign.Center)

            Text('下架后商品将不再展示,已有议价与预定记录将自动取消,24 小时内可重新上架。')
              .fontSize(12)
              .fontColor('#64748B')
              .lineHeight(18)
              .textAlign(TextAlign.Center)

            Row({ space: 10 }) {
              Text('再想想')
                .fontSize(13)
                .fontColor('#64748B')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showOff = false
                })

              Text('确认下架')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#EF4444')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.applyOff()
                })
            }
            .width('100%')
          }
          .padding(18)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('84%')
        }

        if (this.showAsk) {
          modalOverlay(() => {
            this.showAsk = false
          })
          Column({ space: 10 }) {
            Text('出谷攻略')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            Column({ space: 6 }) {
              Text('① 拍清六面图')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF6A00')
              Text('正面 / 背面 / 吧唧别针 / 色纸签名位都要拍,减少纠纷')
                .fontSize(11)
                .fontColor('#64748B')
            }
            .alignItems(HorizontalAlign.Start)
            .width('100%')
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#FFF7ED')

            Column({ space: 6 }) {
              Text('② 定价参考行情')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#2563EB')
              Text('挂价高于近 7 日成交均价 15% 时,曝光会明显下降')
                .fontSize(11)
                .fontColor('#64748B')
            }
            .alignItems(HorizontalAlign.Start)
            .width('100%')
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#EFF6FF')

            Column({ space: 6 }) {
              Text('③ 包装要硬核')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#8B5CF6')
              Text('吧唧套气泡膜,立牌夹硬纸板,色纸装卡套再发货')
                .fontSize(11)
                .fontColor('#64748B')
            }
            .alignItems(HorizontalAlign.Start)
            .width('100%')
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#F5F3FF')

            Text('我知道了')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .padding({ left: 20, right: 20, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor('#FF6A00')
              .width('100%')
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showAsk = false
              })
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('90%')
          .constraintSize({ maxHeight: '80%' })
        }
      }
      .padding(12)
    }
    .backgroundColor('#F5F7FF')
    .layoutWeight(1)
  }
}

const ORDER_ROWS: OrderRow[] = [
  { id: 'GD2026082401', goods: '程小时 吧唧·雨夜', grade: 'SSR', price: 68, num: 1, status: '待收货', statusColor: '#FF6A00', seller: '时光小卖部' },
  { id: 'GD2026082307', goods: '五条悟 Q版立牌', grade: 'SSR', price: 88, num: 1, status: '已发货', statusColor: '#2563EB', seller: '咒术周边屋' },
  { id: 'GD2026082203', goods: '影山飞雄 拼图吧唧', grade: 'SR', price: 35, num: 2, status: '已完结', statusColor: '#16A34A', seller: '排球竞技谷' },
  { id: 'GD2026082115', goods: '花城 色纸·血雨探花', grade: 'SSR', price: 128, num: 1, status: '退款中', statusColor: '#EF4444', seller: '天官阁' },
  { id: 'GD2026082019', goods: '太宰治 亚克力钥匙扣', grade: 'SR', price: 42, num: 1, status: '已完结', statusColor: '#16A34A', seller: '文豪古本店' },
  { id: 'GD2026081922', goods: '陆光 镭射票根', grade: 'R', price: 19, num: 3, status: '已完结', statusColor: '#16A34A', seller: '时光小卖部' }
]

const ORDER_FILTERS: string[] = ['全部', '待收货', '退款/售后', '已完结']

const WALLET_CARDS: WalletCard[] = [
  { id: 1, title: '可用余额', value: '¥1,286.50', desc: '可提现' },
  { id: 2, title: '冻结中', value: '¥340.00', desc: '待确认收货' },
  { id: 3, title: '谷子积分', value: '8,620', desc: '可兑换包材' }
]

const BADGE_LIST: BadgeWall[] = [
  { id: 1, name: '初食谷', lit: true, color: '#FF6A00' },
  { id: 2, name: '吧唧控', lit: true, color: '#2563EB' },
  { id: 3, name: '立牌党', lit: true, color: '#8B5CF6' },
  { id: 4, name: '色纸收藏家', lit: false, color: '#9CA3AF' },
  { id: 5, name: '百单大佬', lit: true, color: '#16A34A' },
  { id: 6, name: '闪电发货', lit: false, color: '#9CA3AF' },
  { id: 7, name: '诚信卖家', lit: true, color: '#F59E0B' },
  { id: 8, name: '年度吃谷王', lit: false, color: '#9CA3AF' }
]

const MSG_LIST: MsgMeta[] = [
  { id: 1, title: '议价提醒', content: '有人对你的「五条悟 Q版立牌」出价 ¥75', time: '2分钟前', read: false },
  { id: 2, title: '上新预告', content: '今晚 20:00 时光代理人系列补货 200 件', time: '1小时前', read: false },
  { id: 3, title: '物流更新', content: '「咒术周边屋」已发货,顺丰 SF1328...', time: '昨天 18:20', read: true },
  { id: 4, title: '行情播报', content: '天官赐福色纸今日上涨 4.8%,注意仓位', time: '昨天 09:00', read: true }
]

// ==================== Tab 5 订单 ====================
@Component
export struct GOrderTab {
  @State ordIdx: number = 0
  @State showDetail: boolean = false
  @State showAfter: boolean = false
  @State showLog: boolean = false
  @State detailIdx: number = 0
  @State reasonIdx: number = -1

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 8 }) {
          ForEach(ORDER_FILTERS, (f: string, i: number) => {
            Text(f)
              .fontSize(12)
              .fontWeight(this.ordIdx === i ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.ordIdx === i ? '#FFFFFF' : '#64748B')
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .borderRadius(14)
              .backgroundColor(this.ordIdx === i ? '#FF6A00' : '#FFFFFF')
              .shadow(this.ordIdx === i ? { radius: 6, color: 'rgba(255,106,0,0.3)', offsetX: 0, offsetY: 3 } : { radius: 2, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 1 })
              .onClick(() => {
                this.ordIdx = i
              })
          }, (f: string) => f)
        }
        .width('100%')

        ForEach(ORDER_ROWS, (o: OrderRow, idx: number) => {
          Column({ space: 10 }) {
            Row({ space: 6 }) {
              Text(o.seller)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1F2937')
                .layoutWeight(1)

              Text(o.status)
                .fontSize(11)
                .fontColor(o.statusColor)
            }
            .width('100%')

            Row({ space: 10 }) {
              Column()
                .width(4)
                .height(52)
                .borderRadius(2)
                .backgroundColor(gradeColor(o.grade))

              Column({ space: 4 }) {
                Text(o.goods)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#1F2937')
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Text('订单号 ' + o.id)
                  .fontSize(10)
                  .fontColor('#94A3B8')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              Column({ space: 3 }) {
                Text('¥' + o.price)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF6A00')
                Text('x' + o.num)
                  .fontSize(10)
                  .fontColor('#94A3B8')
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')

            Row({ space: 8 }) {
              Text(o.grade)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                .borderRadius(8)
                .backgroundColor(gradeColor(o.grade))

              Row()
                .layoutWeight(1)

              Text('查物流')
                .fontSize(11)
                .fontColor('#2563EB')
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .borderRadius(12)
                .border({ width: 1, color: '#BFDBFE' })
                .onClick(() => {
                  this.detailIdx = idx
                  this.showLog = true
                })

              Text('售后')
                .fontSize(11)
                .fontColor('#EF4444')
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .borderRadius(12)
                .border({ width: 1, color: '#FECACA' })
                .onClick(() => {
                  this.detailIdx = idx
                  this.reasonIdx = -1
                  this.showAfter = true
                })

              Text('详情')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .borderRadius(12)
                .backgroundColor('#FF6A00')
                .onClick(() => {
                  this.detailIdx = idx
                  this.showDetail = true
                })
            }
            .width('100%')
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor('#FFFFFF')
          .shadow({ radius: 4, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
        }, (o: OrderRow) => o.id)

        if (this.showDetail) {
          modalOverlay(() => {
            this.showDetail = false
          })
          Column({ space: 12 }) {
            Text('订单详情')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            Column({ space: 6 }) {
              Row({ space: 6 }) {
                Text('商品')
                  .fontSize(12)
                  .fontColor('#94A3B8')
                  .width(64)
                Text(ORDER_ROWS[this.detailIdx].goods)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#1F2937')
                  .layoutWeight(1)
              }
              .width('100%')

              Row({ space: 6 }) {
                Text('卖家')
                  .fontSize(12)
                  .fontColor('#94A3B8')
                  .width(64)
                Text(ORDER_ROWS[this.detailIdx].seller)
                  .fontSize(12)
                  .fontColor('#1F2937')
                  .layoutWeight(1)
              }
              .width('100%')

              Row({ space: 6 }) {
                Text('数量')
                  .fontSize(12)
                  .fontColor('#94A3B8')
                  .width(64)
                Text('x' + ORDER_ROWS[this.detailIdx].num)
                  .fontSize(12)
                  .fontColor('#1F2937')
                  .layoutWeight(1)
              }
              .width('100%')

              Row({ space: 6 }) {
                Text('实付')
                  .fontSize(12)
                  .fontColor('#94A3B8')
                  .width(64)
                Text('¥' + ORDER_ROWS[this.detailIdx].price * ORDER_ROWS[this.detailIdx].num)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF6A00')
                  .layoutWeight(1)
              }
              .width('100%')
            }
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#F8FAFC')

            Text('确认收货后货款将结算给卖家,请先验货再确认')
              .fontSize(10)
              .fontColor('#94A3B8')
              .width('100%')

            Text('好的')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .padding({ left: 20, right: 20, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor('#FF6A00')
              .width('100%')
              .textAlign(TextAlign.Center)
              .onClick(() => {
                this.showDetail = false
              })
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('88%')
        }

        if (this.showAfter) {
          modalOverlay(() => {
            this.showAfter = false
          })
          Column({ space: 12 }) {
            Text('申请售后')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')

            Text('选择售后原因')
              .fontSize(12)
              .fontColor('#475569')
              .width('100%')

            Column({ space: 8 }) {
              Text('商品与描述不符')
                .fontSize(12)
                .fontColor(this.reasonIdx === 0 ? '#FF6A00' : '#475569')
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .borderRadius(10)
                .backgroundColor(this.reasonIdx === 0 ? '#FFF3E8' : '#F8FAFC')
                .width('100%')
                .onClick(() => {
                  this.reasonIdx = 0
                })

              Text('运输破损')
                .fontSize(12)
                .fontColor(this.reasonIdx === 1 ? '#FF6A00' : '#475569')
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .borderRadius(10)
                .backgroundColor(this.reasonIdx === 1 ? '#FFF3E8' : '#F8FAFC')
                .width('100%')
                .onClick(() => {
                  this.reasonIdx = 1
                })

              Text('发错货 / 漏发')
                .fontSize(12)
                .fontColor(this.reasonIdx === 2 ? '#FF6A00' : '#475569')
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .borderRadius(10)
                .backgroundColor(this.reasonIdx === 2 ? '#FFF3E8' : '#F8FAFC')
                .width('100%')
                .onClick(() => {
                  this.reasonIdx = 2
                })

              Text('不想要了(未发货)')
                .fontSize(12)
                .fontColor(this.reasonIdx === 3 ? '#FF6A00' : '#475569')
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .borderRadius(10)
                .backgroundColor(this.reasonIdx === 3 ? '#FFF3E8' : '#F8FAFC')
                .width('100%')
                .onClick(() => {
                  this.reasonIdx = 3
                })
            }
            .width('100%')

            Row({ space: 10 }) {
              Text('取消')
                .fontSize(13)
                .fontColor('#64748B')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor('#F1F5F9')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  this.showAfter = false
                })

              Text('提交申请')
                .fontSize(13)
                .fontColor('#FFFFFF')
                .padding({ left: 20, right: 20, top: 8, bottom: 8 })
                .borderRadius(16)
                .backgroundColor(this.reasonIdx === -1 ? '#FCA5A5' : '#EF4444')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .onClick(() => {
                  if (this.reasonIdx !== -1) {
                    this.showAfter = false
                  }
                })
            }
            .width('100%')
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('90%')
          .constraintSize({ maxHeight: '80%' })
        }

        if (this.showLog) {
          modalOverlay(() => {
            this.showLog = false
          })
          Column({ space: 0 }) {
            Text('物流轨迹')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1F2937')
              .width('100%')
              .padding({ bottom: 12 })

            Column({ space: 0 }) {
              Row({ space: 10 }) {
                Column()
                  .width(10)
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#FF6A00')

                Column({ space: 2 }) {
                  Text('已下单')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FF6A00')
                  Text('08-23 10:12 · 时光小卖部已接单')
                    .fontSize(10)
                    .fontColor('#94A3B8')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')
              .padding({ bottom: 14 })

              Row({ space: 10 }) {
                Column()
                  .width(10)
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#FF6A00')

                Column({ space: 2 }) {
                  Text('打包完成')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FF6A00')
                  Text('08-23 16:40 · 气泡膜+硬纸板包装')
                    .fontSize(10)
                    .fontColor('#94A3B8')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')
              .padding({ bottom: 14 })

              Row({ space: 10 }) {
                Column()
                  .width(10)
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#BFDBFE')

                Column({ space: 2 }) {
                  Text('运输中')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#2563EB')
                  Text('08-24 08:15 · 顺丰速运已揽收')
                    .fontSize(10)
                    .fontColor('#94A3B8')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')
              .padding({ bottom: 14 })

              Row({ space: 10 }) {
                Column()
                  .width(10)
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#E2E8F0')

                Column({ space: 2 }) {
                  Text('派送中')
                    .fontSize(12)
                    .fontColor('#94A3B8')
                  Text('预计 08-25 送达')
                    .fontSize(10)
                    .fontColor('#94A3B8')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')
            }
            .width('100%')

            Text('关闭')
              .fontSize(13)
              .fontColor('#2563EB')
              .padding({ left: 20, right: 20, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor('#EFF4FF')
              .width('100%')
              .textAlign(TextAlign.Center)
              .margin({ top: 12 })
              .onClick(() => {
                this.showLog = false
              })
          }
          .padding(16)
          .borderRadius(16)
          .backgroundColor('#FFFFFF')
          .width('88%')
        }
      }
      .padding(12)
    }
    .backgroundColor('#F5F7FF')
    .layoutWeight(1)
  }
}

const MAIN_TABS: TabMeta[] = [
  { name: '今日谷', icon: '🥞' },
  { name: '行情', icon: '📈' },
  { name: '求购', icon: '📢' },
  { name: '出谷', icon: '📤' },
  { name: '订单', icon: '📋' },
  { name: '我的', icon: '👤' }
]

// ==================== Tab 6 我的 ====================
@Component
export struct GMineTab {
  @State showWithdraw: boolean = false
  @State showBadge: boolean = false
  @State showSetting: boolean = false
  @State channelIdx: number = 0
  @State drawAmount: string = ''

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 12 }) {
          Row({ space: 12 }) {
            Column()
              .width(56)
              .height(56)
              .borderRadius(28)
              .backgroundColor('rgba(255,255,255,0.3)')
              .border({ width: 2, color: '#FFFFFF' })
              .justifyContent(FlexAlign.Center)

            Column({ space: 5 }) {
              Row({ space: 6 }) {
  }
}


总结

在这里插入图片描述

本文对基于 HarmonyOS ArkTS API 24 的谷子集市二次元周边交易平台进行了全面的代码级深度解析。该应用围绕"吃谷"文化构建了从商品浏览、行情分析、求购发布、寄售管理到订单售后的完整交易闭环,六个功能模块各司其职又相互衔接。应用在数据层通过强类型接口定义保障了编译期安全,在视图层大量运用了 ForEachFlexStacklinearGradient 等声明式能力实现了丰富的动态 UI 效果,在交互层通过条件渲染和状态管理实现了弹窗、表单校验、列表增删改查等复杂业务逻辑。

从技术亮点角度来归纳,该应用展现了几个突出的工程实践。首先是不可变状态更新模式的贯彻使用——所有列表的增删改操作都采用"创建新数组-遍历复制-条件修改-整体赋值"的模式,保证了状态更新的可预测性和 ArkTS 声明式框架的响应式更新机制能够正确触发。其次是公共逻辑的高度复用——gradeColor 函数被四个 Tab 组件的十余处调用、modalOverlay Builder 被所有弹窗共享、trendBarH 工具函数被行情页调用,有效降低了代码冗余。再次是视觉反馈的精心设计——档位颜色系统、涨跌红绿配色、物流节点渐进色变、售后按钮的可用性色变,每一处颜色变化都承载着明确的业务语义。

Logo

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

更多推荐