# 鸿蒙ArkTS实战:折扣计算器 — 快速百分比选择与省钱明细展示

一、应用概述

折扣计算器(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 使用 @Propprivate 传递静态数据 减少响应式追踪开销
按需渲染 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 功能扩展

  1. 多商品汇总:添加商品列表,计算总折扣和节省
  2. 税率计算:支持添加税率选项(含税/不含税)
  3. 优惠券叠加:支持多重优惠叠加计算
  4. 历史价格:保存不同商品的折扣方案,方便复用
  5. 汇率转换:支持 USD/EUR/JPY 等多币种显示

7.2 鸿蒙特有能力

  • 元服务卡片:桌面卡片直接展示常用折扣计算入口
  • 拖拽计算:从购物应用拖拽金额到计算器
  • 分布式协同:手机端输入价格,平板端展示详细报表
  • 语音输入:通过语音直接说出「原价 299 打 7 折」自动计算

八、总结

本文构建了一个基于 ArkTS 的折扣计算器,核心技术收获:

  1. 双模式设计:折扣模式 + 满减模式,覆盖主流购物场景
  2. 快速选择交互:预设按钮 + 自定义输入,兼顾效率与灵活性
  3. 即时计算:任何输入变化立即重算,无需手动触发
  4. 省钱明细展示:原价、折后价、节省金额、节省百分比一站式展示
  5. 结果分享:一键复制格式化结果,便于分享到聊天应用

折扣计算器看起来是一个简单的工具应用,但 ArkTS 的声明式框架让数据流、UI 更新、动画过渡的处理变得异常简洁------核心业务代码与 UI 代码的分离、组件的合理拆分、状态的管理方式,都体现了良好的工程实践。


参考链接

相关推荐
b130538100491 小时前
HarmonyOS应用《玄象》开发实战:命盘排布 Canvas:四柱干支 + 六十甲子纳音表的同步绘制
harmonyos·鸿蒙
爱写代码的阿森2 小时前
鸿蒙三方库 | harmony-utils之ArrayUtil集合过滤排序与分块详解
华为·harmonyos·鸿蒙·huawei
不言鹅喻2 小时前
HarmonyOS ArkTS 实战:实现一个掷骰子模拟器
华为·harmonyos
LEO111103 小时前
HarmonyOS应用《玄象》开发实战:RenderingContextSettings(true) 抗锯齿对 Canvas 渲染的影响
harmonyos·鸿蒙
爱写代码的阿森3 小时前
鸿蒙三方库 | harmony-utils之ArrayUtil集合操作与去重详解
华为·harmonyos·鸿蒙·huawei
qizayaoshuap4 小时前
# 评分器组件开发实战:HarmonyOS ArkTS 五星评分交互应用解析
华为·交互·harmonyos
b130538100494 小时前
HarmonyOS应用《玄象》开发实战:AiPhotoFengshuiPage AI 拍照风水:@ohos.multimedia.camera 相机调用
harmonyos·鸿蒙
爱写代码的阿森4 小时前
鸿蒙三方库 | harmony-utils之NumberUtil数值精确计算详解
华为·harmonyos·鸿蒙·huawei
b130538100496 小时前
HarmonyOS应用《玄象》开发实战:HomePage 顶部标题栏的 Row + Blank 三段式布局
harmonyos·鸿蒙