在这里插入图片描述
在这里插入图片描述

一、设计目标

本应用是本系列的"毕业设计"——前 12 个应用分别攻克文案、数字、货币、日期、复数、日历、时区等单点,本应用把它们全部塞进一个真实的跨境购物场景:语言、币种、金额、数量、税费、运费、时间戳、订单号同时参与界面渲染。UI 设计要回答三个问题:

  1. 语言与币种是两个独立开关:用户可以把界面设成英文、结算币种选日元(JPY 零小数位)——两个维度自由组合,UI 如何让用户不困惑;
  2. 金额的正确性必须可感知:¥1,299 的耳机换成 $181.86、€168.87、¥27,928(日元无小数)——换算结果要让人"觉得对",而不是一堆裸数字;
  3. 信息层级在结算页必须零歧义:小计、税费、运费、满减、合计五层费用,加上支付状态切换(待支付→处理中→已支付),UI 要保证用户永远知道"现在该看哪里"。

因此本页 UI 的核心原则是:两个全局状态(语言/币种)+ 一套金额管线——所有价格永远走 fmtMoney() 生成,界面只负责层级与反馈。

二、页面信息架构(组件树)

BillFormatPage(@Entry 根组件)
└─ Column(高 100%,背景 #F8FAFC)
   ├─ Row:标题栏("环球购" + 副标题 + 🛍️ 图标)
   ├─ Column:语言 & 币种选择卡(白色卡片,两行)
   │    ├─ Row:界面语言(标签 + 5 个徽章,选中 teal #0F766E)
   │    └─ Row:结算币种(标签 + 4 个徽章,选中 amber #D97706)
   ├─ Row:汇率提示条(💱 + 文案 + "1 CNY ≈ $0.14",琥珀底 #FEF3C7)
   ├─ Row:Tab 切换(商品市场 / 购物车 (n),下划线分隔)
   └─ 内容区(二选一)
        ├─ MarketView:6 张商品卡(emoji + 名称 + 描述 + 单价 + 加购按钮)
        └─ CartView:购物车行 + 费用明细卡 + 支付状态横幅 + 底部结算栏

架构要点:选择器(语言/币种)全局常驻顶部,Tab 之下是内容区——两个全局状态的变化会同时驱动两个 Tab 的重渲染,用户在任何页面都能感知"我换了语言/币种"。

三、色彩与字号规范

本页用双强调色——语言用 teal(青绿)、币种用 amber(琥珀),两组选择器并排不混淆:

元素 色值 字号 字重
页面标题 #0F172A 22 Bold
副标题 #94A3B8 12 Normal
选择器标签 #64748B 12 Normal
语言选中徽章 文字 #FFF / 底 #0F766E 11 Normal
币种选中徽章 文字 #FFF / 底 #D97706 11 Normal
未选中徽章 文字 #475569 / 底 #E2E8F0 11 Normal
商品名 #0F172A 15 Bold
商品描述 #64748B 12 Normal
单价/金额(琥珀) #D97706 16~22 Bold
加购按钮 底 #0F766E / 已加 #10B981 12 Normal
费用明细行 标签 #64748B / 值 #334155 13~14 Normal
满减(红) #DC2626 14 Normal
汇率提示条 文字 #92400E / 底 #FEF3C7 10 Normal
费用卡背景 #FFF7ED
支付成功横幅 文字 #065F46 / 底 #D1FAE5 11~14

色彩规则:语言=青绿、币种=琥珀、金额=琥珀加粗、负向(满减)=红、正向反馈(免运/已付)=绿——五色语义全页一致,与系列"选中=强调色"的约定对齐。

四、双选择器:语言与币种的分离设计

Column({ space: 8 }) {
  Row() {
    Text(this.T(K.langLabel)).fontSize(12).fontColor('#64748B').width('70')
    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
      ForEach(LANGS, (l: LangCfg) => {
        Text(`${l.flag} ${l.name}`)
          .fontSize(11)
          .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(12)
          .backgroundColor(this.currentLocale === l.code ? '#0F766E' : '#E2E8F0')
          .fontColor(this.currentLocale === l.code ? Color.White : '#475569')
          .onClick(() => { this.currentLocale = l.code; })
      }, (l: LangCfg) => l.code)
    }.layoutWeight(1)
  }.width('100%')

  Row() {
    Text(this.T(K.currencyLabel)).fontSize(12).fontColor('#64748B').width('70')
    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
      ForEach(CURRENCIES, (c: CurrencyCfg) => {
        Text(`${c.symbol} ${c.code}`)
          .fontSize(11)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(12)
          .backgroundColor(this.currentCurrency === c.code ? '#D97706' : '#E2E8F0')
          .fontColor(this.currentCurrency === c.code ? Color.White : '#475569')
          .onClick(() => { this.currentCurrency = c.code; })
      }, (c: CurrencyCfg) => c.code)
    }.layoutWeight(1)
  }.width('100%')
}
.width('94%').padding(12).backgroundColor(Color.White).borderRadius(14).margin({ top: 8 })

设计要点:

  • 标签固定宽 70 左对齐,徽章区 layoutWeight(1) 弹性换行——语言名"简体中文"与币种码"JPY"长度差异不挤乱右侧;
  • 双色区分维度:青绿=语言、琥珀=币种——用户记住"颜色=维度"后,两排徽章互不干扰;
  • 币种徽章带符号¥ CNY / $ USD / € EUR / ¥ JPY——CNY 与 JPY 符号相同(¥),必须靠代码区分;
  • 两个状态都持久化@StorageLink(STORAGE_LOCALE) + @StorageLink(STORAGE_CURRENCY) 各自落盘,重启保持"中文界面 + 美元结算"的组合记忆。

五、汇率提示条:把隐含规则显性化

Row({ space: 6 }) {
  Text('💱').fontSize(13)
  Text(`${this.T(K.rateTip)} · 1 CNY ≈ ${this.curCfg().symbol}${this.curCfg().rate}`)
    .fontSize(10).fontColor('#92400E').layoutWeight(1).textAlign(TextAlign.Start)
}
.width('94%').padding(8).backgroundColor('#FEF3C7').borderRadius(10).margin({ top: 8 })
  • 琥珀底 + 深棕字(#FEF3C7/#92400E)延续"警示/说明"语义,与 01 的 RTL 提示条同族;
  • 内容包含当前汇率1 CNY ≈ $0.14)与免责声明(“汇率为演示固定值”)——用户看到金额变化时能定位原因;
  • 这是 09"体系显性化"设计模式在货币维度的复用:用户切币种发现 ¥1,299 变 $181.86,提示条告诉他"因为汇率是 0.14"

六、商品市场卡:名称/描述/单价三层本地化

ForEach(PRODUCTS, (p: Product, i: number) => {
  Column() {
    Row() {
      Text(p.emoji).fontSize(30)
      Column({ space: 4 }) {
        Row({ space: 6 }) {
          Text(productName(this.currentLocale, p))
            .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#0F172A')
          if (p.tag !== '') { Text(p.tag).fontSize(11) }
        }.width('100%').justifyContent(FlexAlign.Start)
        Text(productDesc(this.currentLocale, p))
          .fontSize(12).fontColor('#64748B').textAlign(TextAlign.Start).width('100%')
      }.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
    }.width('100%').alignItems(VerticalAlign.Top)

    Row() {
      Text(`${this.T(K.unitPrice)} `).fontSize(12).fontColor('#64748B')
      Text(this.fmtMoney(p.priceCents))
        .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#D97706')
      Blank()
      Button(this.qty[i] > 0 ? this.T(K.inCart) : this.T(K.addCart))
        .fontSize(12)
        .backgroundColor(this.qty[i] > 0 ? '#10B981' : '#0F766E')
        .fontColor(Color.White).height(30).padding({ left: 14, right: 14 })
        .onClick(() => { this.addToCart(i); })
    }.width('100%').margin({ top: 8 })
  }
  .width('94%').padding(14).backgroundColor(Color.White).borderRadius(14)
}, (p: Product) => p.key)

设计要点:

  • emoji 商品图:🎧⌨️✍️⌚🔊🔋 作为产品视觉,规避真实图片资源的本地化问题(图片文字不可翻译);
  • 名称走 5 语言 Map、描述同步切换——“Noise-cancelling Headphones” 与「ノイズキャンセリングヘッドホン」长度差一倍,layoutWeight(1) 保证不溢出;
  • 单价用 fmtMoney():同一 priceCents(129900 分)在不同币种下显示 ¥1,299.00 / $181.86 / €168.87 / ¥27,928——数字与符号全部由格式化器生成
  • 按钮双态:未加购 teal"加入购物车"、已加购绿"已加入"——点击后即时反馈,且价格行与按钮通过 Blank() 两端对齐。

七、购物车:费用五层 + 结算状态机

购物车费用明细卡是 i18n 综合呈现的核心区:

Column({ space: 8 }) {
  Row() { Text(this.T(K.subtotal)).fontSize(13).fontColor('#64748B').layoutWeight(1)
         Text(this.fmtMoney(this.goodsTotalCents())).fontSize(14).fontColor('#334155') }.width('100%')
  Row() { Text(`${this.T(K.taxRate)} (${TAX_RATE_PERCENT}%)`).fontSize(13).fontColor('#64748B').layoutWeight(1)
         Text(this.fmtMoney(this.taxCents())).fontSize(14).fontColor('#334155') }.width('100%')
  Row() { Text(this.T(K.shipping)).fontSize(13).fontColor('#64748B').layoutWeight(1)
         Text(this.shippingCents() === 0
           ? `${this.T(K.shippingFree)} ${this.T(K.freeTip)}`
           : this.fmtMoney(this.shippingCents()))
           .fontSize(13).fontColor(this.shippingCents() === 0 ? '#10B981' : '#334155')
           .textAlign(TextAlign.End) }.width('100%')
  if (this.discountCents() > 0) {
    Row() { Text(this.T(K.discount)).fontSize(13).fontColor('#64748B').layoutWeight(1)
           Text(`-${this.fmtMoney(this.discountCents())}`).fontSize(14).fontColor('#DC2626') }.width('100%')
  }
  Divider().color('#E2E8F0')
  Row() { Text(this.T(K.total)).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#0F172A').layoutWeight(1)
         Text(this.fmtMoney(this.totalCents()))
           .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#D97706') }.width('100%')
}
.width('94%').padding(14).backgroundColor('#FFF7ED').borderRadius(14)

设计要点:

  • 五层费用从灰到琥珀:小计/税费/运费普通灰,满减红色负号(-¥50.00),合计 22 号琥珀加粗——视觉焦点永远落在合计;
  • 免运费态本地化免运费 满 ¥300 免运费,未满收 ¥12(英文 “Free · Free shipping over ¥300, otherwise ¥12”)——条件文案整句从文案表取,不是拼接
  • 条件行用 if 渲染:满减不满足时不显示该行,Divider 与合计之间层级干净;
  • 底部结算栏:清空按钮 + 合计 + 支付按钮(enabled 控制禁用态),支付状态机三态——待支付(琥珀)/处理中(橙)/已支付(绿),每个状态都有对应文案与颜色

八、支付成功横幅:时间/订单号的综合本地化

if (this.paidAt !== -1) {
  Column({ space: 4 }) {
    Text(`🎉 ${this.T(K.paySuccess)}`).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#065F46')
    Text(this.T(K.paySuccessDesc)).fontSize(11).fontColor('#047857').textAlign(TextAlign.Center)
    Row() { Text(`${this.T(K.orderNo)}: ${this.fmtOrderNo()}`).fontSize(11).fontColor('#065F46') }
      .width('100%').justifyContent(FlexAlign.Center).margin({ top: 4 })
    Row() { Text(`${this.T(K.orderTime)}: ${this.fmtTime(this.paidAt)}`).fontSize(11).fontColor('#065F46') }
      .width('100%').justifyContent(FlexAlign.Center)
    Row() { Text(`${this.T(K.status)}: ${this.T(K.statusPaid)}`).fontSize(11).fontColor('#065F46') }
      .width('100%').justifyContent(FlexAlign.Center)
  }
  .width('94%').padding(12).backgroundColor('#D1FAE5').borderRadius(12)
}
  • 订单号走 NumberFormat2026081912345678 在英文下显示 20,260,819,123,456,78(带千分位)——数字分组也是本地化的一部分;
  • 下单时间走 DateTimeFormat:中文"2026年8月19日 14:30"、英文 “August 19, 2026 at 2:30 PM”——日期时间风格(long + short)由语言模板决定;
  • **绿底横幅(#D1FAE5)**与"已支付"按钮绿色呼应,成功语义全页一致。

九、排版验证清单(多语言下自查)

检查项 方法 验证结果
金额随币种 fmtMoney()style:'currency' ¥1,299 → $181.86 / €168.87 / ¥27,928
日元零小数 CURRENCIES 配置 digits: 0 JPY 显示 ¥27,928 无 .00
商品名随语言 productName() 5 语言 Map 日文名称不溢出卡片
复数规则 fmtQty()PluralRules 英文 “1 item” / “3 items”
订单号分组 NumberFormat 英文带千分位逗号
时间随语言 fmtTime() dateStyle:'long' 中英日三格式正确
免运费态 条件文案从文案表整体取 "满 ¥300 免运费"整句本地化
空购物车态 cartCount() === 0 分支 空态文案 + 去逛逛按钮

十、可访问性(多语言 UI 加分项)

  • 双选择器不靠单色:语言=青绿、币种=琥珀,且徽章带文字(🇨🇳 简体中文 / $ USD)——色弱用户靠文字区分维度;
  • 金额颜色 + 字号双编码:合计 22 号琥珀 vs 小计 14 号灰——不只看颜色也能靠字号定位重点;
  • 状态机反馈完整:支付按钮禁用态(enabled)+ 文案变化(立即支付→支付处理中→已支付),视觉与文本双重反馈;
  • 对比度:琥珀 #D97706 on 白 ≈ 3.9:1(大字 16+ 满足 AA Large)、teal #0F766E on 白 ≈ 5.9:1、绿 #10B981 on 白 ≈ 3.0:1(用于大字/底);
  • 金额可朗读fmtMoney 输出带符号与货币码的完整字符串,朗读器读出 “美元 181 点 86” 而非裸数字。

十一、小结

本页 UI 是系列 13 个应用的综合验收:语言徽章(01 模式)、币种徽章(03 模式)、金额管线(09 模式)、复数数量(05 模式)、时间订单号(02 模式)、状态机反馈(通用模式)在一屏内协同工作。设计上最大的成功是双选择器 + 一条金额管线——用户只操作两个开关,全页几百个数字全部正确刷新,这正是"状态驱动 UI"的威力。下一篇文章深入技术实现,讲解整数分存储、汇率换算、货币格式化的完整链路。

Logo

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

更多推荐