鸿蒙全球性项目实战 -跨过商品账单综合格式化-页面设计
·


一、设计目标
本应用是本系列的"毕业设计"——前 12 个应用分别攻克文案、数字、货币、日期、复数、日历、时区等单点,本应用把它们全部塞进一个真实的跨境购物场景:语言、币种、金额、数量、税费、运费、时间戳、订单号同时参与界面渲染。UI 设计要回答三个问题:
- 语言与币种是两个独立开关:用户可以把界面设成英文、结算币种选日元(JPY 零小数位)——两个维度自由组合,UI 如何让用户不困惑;
- 金额的正确性必须可感知:¥1,299 的耳机换成 $181.86、€168.87、¥27,928(日元无小数)——换算结果要让人"觉得对",而不是一堆裸数字;
- 信息层级在结算页必须零歧义:小计、税费、运费、满减、合计五层费用,加上支付状态切换(待支付→处理中→已支付),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)
}
- 订单号走
NumberFormat:2026081912345678在英文下显示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"的威力。下一篇文章深入技术实现,讲解整数分存储、汇率换算、货币格式化的完整链路。
更多推荐




所有评论(0)