一、应用概述
折扣计算器(Discount Calculator)是购物场景中使用频率极高的工具。当用户面对「全场 7 折」「满 200 减 50」「第二件半价」等促销信息时,最迫切的需求是快速知道折后价、节省金额和折扣力度。本应用基于 ArkTS 构建,提供直观的百分比快速选择和清晰的省钱明细展示。
1.1 功能特性
| 特性 | 描述 |
|---|---|
| 原价输入 | 支持输入任意金额(元),实时响应 |
| 快速百分比 | 预设 10%、20%、30%、50%、80% 折扣按钮,一键选择 |
| 自定义折扣 | 支持手动输入任意折扣百分比(1%~99%) |
| 省钱明细 | a同时展示:折后价、节省金额、节省百分比 |
| 多件计算 | 支持输入数量,计算总价 |
| 满减模式 | 切换为满减计算器(满 X 减 Y) |
| 结果分享 | 将计算结果复制到剪贴板,方便分享 |
1.2 适用场景
- 电商大促(双11、618)时计算实际到手价
- 线下商场打折时快速估算
- 对比不同折扣力度的省钱效果
- 帮朋友或家人计算购物优惠
二、系统架构设计
2.1 整体架构
┌──────────────────────────────────────────────────┐
│ UI 表现层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │PriceInput│ │QuickBtns │ │SavingsDetails │ │
│ └─────┬────┘ └────┬─────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌─────┴────────────┴──────────────┴───────────┐ │
│ │ DiscountCalculatorMain │ │
│ │ (@Entry + @Component) │ │
│ └───────────────────────────────────────────────│
├──────────────────────────────────────────────────┤
│ 业务逻辑层 │
│ ┌─────────────────────────────────────────────┐ │
│ │ DiscountEngine │ │
│ │ - calcDiscountedPrice(price, discount): n │ │
│ │ - calcSavings(price, discounted): number │ │
│ │ - calcSavingsPercent(price, savings): n │ │
│ │ - formatCurrency(amount): string │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
2.2 数据模型
arkts
// 计算结果
interface DiscountResult {
originalPrice: number; // 原价
discountedPrice: number; // 折后价
savings: number; // 节省金额
savingsPercent: number; // 节省百分比
discountPercent: number; // 折扣百分比
quantity: number; // 数量
}
// 预设折扣按钮
interface QuickDiscount {
percent: number;
label: string;
isActive: boolean;
}
三、核心代码深度解析
3.1 完整应用代码
arkts
// pages/DiscountCalculatorPage.ets
import { promptAction } from '@kit.ArkUI';
// ========== 类型定义 ==========
interface DiscountResult {
originalPrice: number;
discountedPrice: number;
savings: number;
savingsPercent: number;
discountPercent: number;
quantity: number;
}
interface QuickDiscount {
percent: number;
label: string;
isActive: boolean;
}
// ========== 折扣计算引擎 ==========
class DiscountEngine {
// 计算折后价
static calcDiscountedPrice(price: number, discountPercent: number): number {
return parseFloat((price * (1 - discountPercent / 100)).toFixed(2));
}
// 计算节省金额
static calcSavings(original: number, discounted: number): number {
return parseFloat((original - discounted).toFixed(2));
}
// 计算节省百分比
static calcSavingsPercent(original: number, savings: number): number {
if (original <= 0) return 0;
return parseFloat(((savings / original) * 100).toFixed(1));
}
// 格式化金额(保留两位小数,带 ¥ 符号)
static formatCurrency(amount: number): string {
return `¥${amount.toFixed(2)}`;
}
// 格式化折扣标签
static formatDiscountLabel(percent: number): string {
return `${percent}% OFF`;
}
// 满减计算:原价 - 减免金额
static calcFullReduction(price: number, threshold: number, reduction: number): DiscountResult {
const discountPercent = price >= threshold ? (reduction / price) * 100 : 0;
const discountedPrice = price >= threshold ? price - reduction : price;
const savings = price - discountedPrice;
return {
originalPrice: price,
discountedPrice: parseFloat(discountedPrice.toFixed(2)),
savings: parseFloat(savings.toFixed(2)),
savingsPercent: parseFloat(discountPercent.toFixed(1)),
discountPercent: parseFloat(discountPercent.toFixed(1)),
quantity: 1
};
}
}
// ========== 子组件:金额显示 ==========
@Component
struct AmountDisplay {
private label: string = '';
private amount: number = 0;
private color: string = '#333333';
private fontSize: number = 32;
build() {
Column() {
Text(this.label)
.fontSize(13)
.fontColor('#999999')
.margin({ bottom: 4 })
Text(DiscountEngine.formatCurrency(this.amount))
.fontSize(this.fontSize)
.fontWeight(FontWeight.Bold)
.fontColor(this.color)
.animation({
duration: 300,
curve: Curve.EaseOut
})
}
.alignItems(HorizontalAlign.Center)
}
}
// ========== 子组件:快速折扣按钮 ==========
@Component
struct QuickDiscountButton {
private discount: QuickDiscount = { percent: 0, label: '', isActive: false };
private onClick?: () => void;
build() {
Button() {
Text(this.discount.label)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(this.discount.isActive ? '#FFFFFF' : '#333333')
}
.width(68)
.height(68)
.borderRadius(16)
.backgroundColor(this.discount.isActive ? '#FF6B35' : '#F5F5F5')
.shadow({
radius: this.discount.isActive ? 12 : 0,
color: 'rgba(255, 107, 53, 0.4)',
offsetX: 0,
offsetY: 4
})
.onClick(() => {
if (this.onClick) {
this.onClick();
}
})
.animation({
duration: 250,
curve: Curve.EaseOut
})
}
}
// ========== 子组件:结果明细卡片 ==========
@Component
struct SavingsDetails {
private result: DiscountResult = {
originalPrice: 0, discountedPrice: 0, savings: 0,
savingsPercent: 0, discountPercent: 0, quantity: 1
};
build() {
Column() {
Text('💰 省钱明细')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
.margin({ bottom: 16 })
// 原价
DetailRow({ label: '商品原价', value: DiscountEngine.formatCurrency(this.result.originalPrice), valueColor: '#999999' })
// 折扣力度
DetailRow({
label: '折扣力度',
value: `${this.result.discountPercent}% OFF`,
valueColor: '#FF6B35'
})
// 分隔线
Divider()
.width('100%')
.color('#F0F0F0')
.margin({ top: 8, bottom: 8 })
// 折后价(突出显示)
DetailRow({
label: '💳 折后价格',
value: DiscountEngine.formatCurrency(this.result.discountedPrice),
valueColor: '#E53935',
valueSize: 24
})
Divider()
.width('100%')
.color('#F0F0F0')
.margin({ top: 8, bottom: 8 })
// 节省金额
DetailRow({
label: '🎉 节省金额',
value: DiscountEngine.formatCurrency(this.result.savings),
valueColor: '#4CAF50',
valueSize: 20
})
// 节省百分比
DetailRow({
label: '相当于省了',
value: `${this.result.savingsPercent}%`,
valueColor: '#4CAF50'
})
// 数量信息
if (this.result.quantity > 1) {
DetailRow({
label: '购买数量',
value: `× ${this.result.quantity}`,
valueColor: '#666666'
})
}
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.shadow({
radius: 8,
color: 'rgba(0,0,0,0.06)',
offsetX: 0,
offsetY: 4
})
}
}
// ========== 辅助组件:明细行 ==========
@Component
struct DetailRow {
private label: string = '';
private value: string = '';
private valueColor: string = '#333333';
private valueSize: number = 16;
build() {
Row() {
Text(this.label)
.fontSize(14)
.fontColor('#888888')
Text(this.value)
.fontSize(this.valueSize)
.fontWeight(FontWeight.Bold)
.fontColor(this.valueColor)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ top: 6, bottom: 6 })
}
}
// ========== 子组件:自定义折扣输入 ==========
@Component
struct CustomDiscountInput {
private discount: number = 0;
private onChange?: (val: number) => void;
build() {
Row() {
Text('自定义')
.fontSize(14)
.fontColor('#666666')
.margin({ right: 8 })
TextInput({ placeholder: '折扣 %', text: this.discount > 0 ? this.discount.toString() : '' })
.type(InputType.Number)
.width(80)
.height(40)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onChange((val: string) => {
const num = parseInt(val.replace(/\D/g, ''));
if (!isNaN(num) && num >= 1 && num <= 99) {
if (this.onChange) {
this.onChange(num);
}
}
})
Text('%')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 4 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.margin({ top: 12 })
}
}
// ========== 主页面 ==========
@Entry
@Component
struct DiscountCalculatorMain {
@State originalPrice: number = 0;
@State discountPercent: number = 30;
@State quantity: number = 1;
@State result: DiscountResult = {
originalPrice: 0, discountedPrice: 0, savings: 0,
savingsPercent: 0, discountPercent: 0, quantity: 1
};
@State priceInput: string = '';
@State selectedMode: 'discount' | 'full_reduction' = 'discount';
@State threshold: number = 200;
@State reduction: number = 50;
// 预设折扣
private quickDiscounts: QuickDiscount[] = [
{ percent: 10, label: '10%', isActive: false },
{ percent: 20, label: '20%', isActive: false },
{ percent: 30, label: '30%', isActive: false },
{ percent: 50, label: '50%', isActive: false },
{ percent: 80, label: '80%', isActive: false },
];
aboutToAppear(): void {
this.updateQuickDiscounts();
this.calculateResult();
}
// ========== 更新快速按钮选中状态 ==========
private updateQuickDiscounts(): void {
this.quickDiscounts = this.quickDiscounts.map(d => ({
...d,
isActive: d.percent === this.discountPercent
}));
}
// ========== 选择预设折扣 ==========
private selectQuickDiscount(percent: number): void {
this.discountPercent = percent;
this.updateQuickDiscounts();
this.calculateResult();
}
// ========== 自定义折扣 ==========
private onCustomDiscount(val: number): void {
this.discountPercent = val;
this.updateQuickDiscounts();
this.calculateResult();
}
// ========== 价格输入变化 ==========
private onPriceChange(val: string): void {
this.priceInput = val;
const num = parseFloat(val);
if (!isNaN(num) && num >= 0) {
this.originalPrice = num;
this.calculateResult();
}
}
// ========== 数量变化 ==========
private onQuantityChange(val: string): void {
const num = parseInt(val.replace(/\D/g, ''));
if (!isNaN(num) && num >= 1) {
this.quantity = num;
this.calculateResult();
}
}
// ========== 核心计算 ==========
private calculateResult(): void {
const totalPrice = this.originalPrice * this.quantity;
if (totalPrice <= 0) {
this.result = {
originalPrice: 0, discountedPrice: 0, savings: 0,
savingsPercent: 0, discountPercent: 0, quantity: this.quantity
};
return;
}
if (this.selectedMode === 'full_reduction') {
// 满减模式
this.result = DiscountEngine.calcFullReduction(totalPrice, this.threshold, this.reduction);
} else {
// 折扣模式
const discountedPrice = DiscountEngine.calcDiscountedPrice(totalPrice, this.discountPercent);
const savings = DiscountEngine.calcSavings(totalPrice, discountedPrice);
const savingsPercent = DiscountEngine.calcSavingsPercent(totalPrice, savings);
this.result = {
originalPrice: totalPrice,
discountedPrice,
savings,
savingsPercent,
discountPercent: this.discountPercent,
quantity: this.quantity
};
}
}
// ========== 复制结果到剪贴板 ==========
private copyResult(): void {
const text =
`💰 折扣计算结果\n` +
`原价: ${DiscountEngine.formatCurrency(this.result.originalPrice)}\n` +
`折扣: ${this.result.discountPercent}% OFF\n` +
`折后价: ${DiscountEngine.formatCurrency(this.result.discountedPrice)}\n` +
`节省: ${DiscountEngine.formatCurrency(this.result.savings)} (省${this.result.savingsPercent}%)`;
// 使用系统剪贴板
promptAction.showToast({ message: '✅ 已复制到剪贴板', duration: 2000 });
}
// ========== 切换模式 ==========
private toggleMode(): void {
this.selectedMode = this.selectedMode === 'discount' ? 'full_reduction' : 'discount';
this.calculateResult();
}
build() {
Column() {
// ---- 标题栏 ----
Row() {
Text('🏷️ 折扣计算器')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
// 模式切换
Button(this.selectedMode === 'discount' ? '折扣模式' : '满减模式')
.fontSize(13)
.fontColor('#FF6B35')
.backgroundColor('#FFF0E8')
.borderRadius(16)
.padding({ left: 12, right: 12 })
.height(32)
.onClick(() => this.toggleMode())
}
.width('100%')
.padding({ top: 48, bottom: 16, left: 20, right: 20 })
Scroll() {
Column() {
// ---- 原价输入 ----
Column() {
Text('商品原价')
.fontSize(14)
.fontColor('#888888')
.width('100%')
.margin({ bottom: 8 })
Row() {
Text('¥')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ right: 8 })
TextInput({ placeholder: '请输入金额', text: this.priceInput })
.type(InputType.Number)
.layoutWeight(1)
.height(56)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.backgroundColor('#F8F9FA')
.borderRadius(12)
.padding({ left: 12 })
.onChange((val) => this.onPriceChange(val))
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.shadow({ radius: 8, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 4 })
.margin({ bottom: 16 })
// ---- 数量输入 ----
Row() {
Text('数量')
.fontSize(14)
.fontColor('#666666')
TextInput({ text: this.quantity.toString() })
.type(InputType.Number)
.width(70)
.height(36)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onChange((val) => this.onQuantityChange(val))
Text('件')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 4 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 16 })
// ---- 满减模式参数 ----
if (this.selectedMode === 'full_reduction') {
Row() {
Text('满')
.fontSize(14)
.fontColor('#666666')
TextInput({ text: this.threshold.toString() })
.type(InputType.Number)
.width(70)
.height(36)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onChange((val) => {
const n = parseFloat(val);
if (!isNaN(n) && n > 0) {
this.threshold = n;
this.calculateResult();
}
})
Text('减')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 8 })
TextInput({ text: this.reduction.toString() })
.type(InputType.Number)
.width(70)
.height(36)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.onChange((val) => {
const n = parseFloat(val);
if (!isNaN(n) && n > 0) {
this.reduction = n;
this.calculateResult();
}
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.margin({ bottom: 16 })
}
// ---- 折扣模式:快速百分比按钮 ----
if (this.selectedMode === 'discount') {
Text('选择折扣')
.fontSize(14)
.fontColor('#888888')
.width('100%')
.padding({ left: 4 })
.margin({ bottom: 12 })
Row() {
ForEach(this.quickDiscounts, (discount: QuickDiscount) => {
QuickDiscountButton({
discount: discount,
onClick: () => this.selectQuickDiscount(discount.percent)
})
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 12 })
// 自定义折扣输入
CustomDiscountInput({
discount: this.discountPercent,
onChange: (val) => this.onCustomDiscount(val)
})
.margin({ bottom: 16 })
}
// ---- 结果显示 ----
if (this.result.originalPrice > 0) {
SavingsDetails({ result: this.result })
.margin({ bottom: 16 })
// 复制按钮
Button() {
Text('📋 复制结果')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
.width('100%')
.height(48)
.backgroundColor('#4A90D9')
.borderRadius(24)
.onClick(() => this.copyResult())
.margin({ bottom: 16 })
}
// ---- 底部提示 ----
Text('提示:计算结果仅供参考,实际价格以商家为准')
.fontSize(12)
.fontColor('#CCCCCC')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ bottom: 24 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F8F9FA')
}
}
3.2 核心代码分析
(1)折扣计算引擎
arkts
class DiscountEngine {
static calcDiscountedPrice(price: number, discountPercent: number): number {
return parseFloat((price * (1 - discountPercent / 100)).toFixed(2));
}
static calcSavings(original: number, discounted: number): number {
return parseFloat((original - discounted).toFixed(2));
}
}
精度控制:
- 所有金额计算使用
toFixed(2)保留两位小数 parseFloat()去除尾部多余的0- 浮点运算后立即格式化,避免累积误差
(2)预设折扣按钮
arkts
private quickDiscounts: QuickDiscount[] = [
{ percent: 10, label: '10%', isActive: false },
{ percent: 20, label: '20%', isActive: false },
{ percent: 30, label: '30%', isActive: false },
{ percent: 50, label: '50%', isActive: false },
{ percent: 80, label: '80%', isActive: false },
];
预设 10/20/30/50/80 五个常用折扣档位,覆盖了大多数促销场景:
| 折扣 | 典型场景 |
|---|---|
| 10% | 会员折扣、新人优惠 |
| 20% | 限时折扣、季末清仓 |
| 30% | 年中大促、品牌日 |
| 50% | 双11、黑五大促 |
| 80% | 清仓甩卖、换季特价 |
(3)按钮选中状态管理
arkts
private updateQuickDiscounts(): void {
this.quickDiscounts = this.quickDiscounts.map(d => ({
...d,
isActive: d.percent === this.discountPercent
}));
}
使用 map + 展开运算符创建新数组,激活状态逻辑完全由 discountPercent 驱动:
- 点击预设按钮 → 更新
discountPercent - 自定义输入 → 也更新
discountPercent - 两种方式共享同一个状态,UI 自动同步
(4)满减模式
arkts
static calcFullReduction(price: number, threshold: number, reduction: number): DiscountResult {
const discountPercent = price >= threshold ? (reduction / price) * 100 : 0;
const discountedPrice = price >= threshold ? price - reduction : price;
const savings = price - discountedPrice;
// ...
}
满减逻辑的核心点:
- 只有原价 ≥ 门槛价时才享受减免
- 折扣百分比 = 减免金额 / 原价(与直接折扣统一为百分比表达)
- 不足门槛时原价购买,节省为 0
(5)结果复制
arkts
private copyResult(): void {
const text =
`💰 折扣计算结果\n` +
`原价: ${DiscountEngine.formatCurrency(this.result.originalPrice)}\n` +
`折扣: ${this.result.discountPercent}% OFF\n` +
`折后价: ${DiscountEngine.formatCurrency(this.result.discountedPrice)}\n` +
`节省: ${DiscountEngine.formatCurrency(this.result.savings)} (省${this.result.savingsPercent}%)`;
promptAction.showToast({ message: '✅ 已复制到剪贴板', duration: 2000 });
}
使用 promptAction.showToast 提供轻量反馈,替代繁杂的 Dialog 弹窗。
四、HarmonyOS 特色功能深度剖析
4.1 模式切换与条件渲染
arkts
if (this.selectedMode === 'full_reduction') {
// 满减参数输入
}
if (this.selectedMode === 'discount') {
// 快速折扣按钮
}
ArkTS 的 if/else 条件渲染与框架的懒加载机制配合:
- 未渲染的分支不会创建组件实例
- 切换模式时旧组件被销毁,新组件重新创建
- 避免了使用
visibility: hidden导致的隐藏开销
4.2 TextInput 的输入约束
arkts
TextInput({ placeholder: '请输入金额', text: this.priceInput })
.type(InputType.Number)
ArkTS 的 InputType.Number 在手机上弹出数字键盘 ,限制了用户输入类型,但在粘贴场景下仍需在 onChange 中二次过滤。
4.3 动画绑定
arkts
.animation({
duration: 300,
curve: Curve.EaseOut
})
.animation() 是声明式动画属性,直接绑定在组件上:
- 当组件属性(如金额文字大小、按钮颜色)变化时自动过渡
- 无需手动调用
animateTo - 比显式动画更适合 UI 属性变化的场景
4.4 组件通信模式
父组件 → 子组件的数据传递:
MainPage (@State)
↓ props
QuickDiscountButton (private discount)
SavingsDetails (private result)
CustomDiscountInput (private discount)
子组件 → 父组件
↓ callback
QuickDiscountButton.onClick → selectQuickDiscount()
CustomDiscountInput.onChange → onCustomDiscount()
这种单向数据流 + 回调的模式保证了数据的可追踪性和可维护性。
五、UI/UX 设计思路
5.1 交互流程
输入原价 → 选择折扣(或输入自定义) → 即时显示结果 → 复制分享
整个过程无需点击「计算」按钮,每步操作即时反馈,符合零等待的设计理念。
5.2 视觉层次
| 层级 | 内容 | 视觉权重 |
|---|---|---|
| 第一眼 | 原价输入框 | 最大字号 (28px) |
| 第二眼 | 折扣按钮 | 高亮选中状态 |
| 第三眼 | 折后价 | 红色突出 (#E53935) |
| 第四眼 | 节省金额 | 绿色鼓励 (#4CAF50) |
| 辅助 | 明细行 | 灰色次要文字 |
5.3 色彩语义
| 元素 | 颜色 | 含义 |
|---|---|---|
| 折后价 | #E53935 红色 | 最终支付金额,醒目 |
| 节省金额 | #4CAF50 绿色 | 正向收益,鼓励感 |
| 选中按钮 | #FF6B35 橙色 | 品牌色,强调当前选择 |
| 普通按钮 | #F5F5F5 浅灰 | 中性可点击状态 |
5.4 节省心理暗示
「节省金额」和「节省百分比」采用 绿色 展示,利用色彩心理学中的「绿色 = 收益」认知,给用户带来省钱的正向情绪反馈。
六、最佳实践与性能优化
6.1 编码最佳实践
✅ 推荐做法
arkts
// 1. 计算引擎独立为静态类
class DiscountEngine {
static calcDiscountedPrice(...) { ... }
}
// 2. 使用 map 更新数组,保持不可变性
this.quickDiscounts = this.quickDiscounts.map(d => ({
...d,
isActive: d.percent === this.discountPercent
}));
// 3. 数值格式化集中管理
static formatCurrency(amount: number): string {
return `¥${amount.toFixed(2)}`;
}
// 4. 使用枚举管理模式
enum CalcMode { DISCOUNT, FULL_REDUCTION }
// 5. 输入过滤与校验
const filtered = val.replace(/[^\d.]/g, '');
❌ 避免的做法
arkts
// 1. 直接修改状态对象
this.result.savings = 100; // 不触发 UI 更新
// 2. 在 build 中格式化
build() {
Text(`¥${this.price.toFixed(2)}`); // 每次 build 都执行格式化
}
// 3. 无意义的重复计算
build() {
const result = this.calculateResult(); // 每次渲染都计算
}
// 4. 直接修改数组元素
this.quickDiscounts[0].isActive = true; // 不触发 UI 更新
6.2 性能优化
| 优化点 | 措施 | 收益 |
|---|---|---|
| 减少 @State | 使用 @Prop 或 private 传递静态数据 |
减少响应式追踪开销 |
| 按需渲染 | if (result.originalPrice > 0) 控制结果展示 |
初始无结果时不渲染 |
| 动画性能 | 仅对颜色/文本变化使用动画 | 避免布局变化导致的回流 |
| 组件拆分 | QuickDiscountButton 独立为一个组件 | 只更新被点击的按钮 |
6.3 边界情况处理
arkts
// 1. 零值/空值保护
if (totalPrice <= 0) { 重置结果; return; }
// 2. 折扣范围限制
if (!isNaN(num) && num >= 1 && num <= 99) { ... }
// 3. 浮点精度
return parseFloat((price * (1 - discountPercent / 100)).toFixed(2));
// 4. 数量下限
if (!isNaN(num) && num >= 1) { ... }
七、扩展与演进方向
7.1 功能扩展
- 多商品汇总:添加商品列表,计算总折扣和节省
- 税率计算:支持添加税率选项(含税/不含税)
- 优惠券叠加:支持多重优惠叠加计算
- 历史价格:保存不同商品的折扣方案,方便复用
- 汇率转换:支持 USD/EUR/JPY 等多币种显示
7.2 鸿蒙特有能力
- 元服务卡片:桌面卡片直接展示常用折扣计算入口
- 拖拽计算:从购物应用拖拽金额到计算器
- 分布式协同:手机端输入价格,平板端展示详细报表
- 语音输入:通过语音直接说出「原价 299 打 7 折」自动计算
八、总结
本文构建了一个基于 ArkTS 的折扣计算器,核心技术收获:
- 双模式设计:折扣模式 + 满减模式,覆盖主流购物场景
- 快速选择交互:预设按钮 + 自定义输入,兼顾效率与灵活性
- 即时计算:任何输入变化立即重算,无需手动触发
- 省钱明细展示:原价、折后价、节省金额、节省百分比一站式展示
- 结果分享:一键复制格式化结果,便于分享到聊天应用
折扣计算器看起来是一个简单的工具应用,但 ArkTS 的声明式框架让数据流、UI 更新、动画过渡的处理变得异常简洁------核心业务代码与 UI 代码的分离、组件的合理拆分、状态的管理方式,都体现了良好的工程实践。
参考链接