在这里插入图片描述

年龄计算看起来简单,但要精确到年月日就需要考虑很多细节。今天我们用 React Native 实现一个年龄计算器,不仅显示精确年龄,还能告诉你距离下次生日还有多少天。

功能设计

这个年龄计算器需要实现:

  • 输入出生日期
  • 计算精确年龄(年、月、日)
  • 显示已经活了多少天、周、月、小时
  • 显示距离下次生日的天数和进度条

年龄计算的难点在于月份天数不固定,需要正确处理借位。

状态和动画定义

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Animated, ScrollView } from 'react-native';

export const AgeCalculator: React.FC = () => {
  const [birthday, setBirthday] = useState('');
  const [result, setResult] = useState<any>(null);
  
  const scaleAnim = useRef(new Animated.Value(0)).current;
  const rotateAnim = useRef(new Animated.Value(0)).current;
  const pulseAnim = useRef(new Animated.Value(1)).current;
  const progressAnim = useRef(new Animated.Value(0)).current;

状态变量:

  • birthday 存储用户输入的出生日期
  • result 存储计算结果

动画值:

  • scaleAnim 控制结果卡片的弹出效果
  • pulseAnim 控制头部图标的脉冲效果
  • progressAnim 控制进度条的填充动画

鸿蒙 ArkTS 对比:状态定义

@Entry
@Component
struct AgeCalculator {
  @State birthday: string = ''
  @State result: AgeResult | null = null
  @State scaleValue: number = 0
  @State progressValue: number = 0

interface AgeResult {
  years: number
  months: number
  days: number
  totalDays: number
  totalWeeks: number
  totalMonths: number
  totalHours: number
  daysToNext: number
  progressToNext: number
}

ArkTS 中用接口定义结果类型,让代码更清晰。

头部脉冲动画

  useEffect(() => {
    Animated.loop(
      Animated.sequence([
        Animated.timing(pulseAnim, { toValue: 1.1, duration: 800, useNativeDriver: true }),
        Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true }),
      ])
    ).start();
  }, []);

蛋糕图标有轻微的脉冲效果,1.6 秒一个周期,让界面更有活力。

核心年龄计算逻辑

  const calculate = () => {
    const birth = new Date(birthday);
    if (isNaN(birth.getTime())) {
      setResult({ error: '请输入有效日期格式 (YYYY-MM-DD)' });
      return;
    }

    const today = new Date();
    let years = today.getFullYear() - birth.getFullYear();
    let months = today.getMonth() - birth.getMonth();
    let days = today.getDate() - birth.getDate();
    
    if (days < 0) {
      months--;
      days += new Date(today.getFullYear(), today.getMonth(), 0).getDate();
    }
    if (months < 0) {
      years--;
      months += 12;
    }

年龄计算的关键是处理借位:

  1. 如果日期差为负数,从月份借一个月,加上上个月的天数
  2. 如果月份差为负数,从年份借一年,加上 12 个月

new Date(year, month, 0).getDate() 是获取上个月天数的技巧,传入 0 作为日期会返回上个月的最后一天。

鸿蒙 ArkTS 对比:年龄计算

calculateAge() {
  let birth = new Date(this.birthday)
  if (isNaN(birth.getTime())) {
    this.result = { error: '请输入有效日期格式' } as AgeResult
    return
  }
  
  let today = new Date()
  let years = today.getFullYear() - birth.getFullYear()
  let months = today.getMonth() - birth.getMonth()
  let days = today.getDate() - birth.getDate()
  
  // 处理借位
  if (days < 0) {
    months--
    let lastMonth = new Date(today.getFullYear(), today.getMonth(), 0)
    days += lastMonth.getDate()
  }
  if (months < 0) {
    years--
    months += 12
  }
  
  // ... 其他计算
}

计算逻辑完全一样,JavaScript 的日期处理在 ArkTS 中同样适用。

统计数据计算

    const totalDays = Math.floor((today.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24));
    const totalWeeks = Math.floor(totalDays / 7);
    const totalMonths = years * 12 + months;
    const totalHours = totalDays * 24;

这些统计数据计算比较直接:

  • 总天数:时间差除以一天的毫秒数
  • 总周数:总天数除以 7
  • 总月数:年数乘以 12 加上月数
  • 总小时数:总天数乘以 24

下次生日计算

    const nextBirthday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
    if (nextBirthday < today) nextBirthday.setFullYear(nextBirthday.getFullYear() + 1);
    const daysToNext = Math.ceil((nextBirthday.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
    const progressToNext = ((365 - daysToNext) / 365) * 100;

下次生日的计算:

  1. 用今年的年份和出生的月日创建日期
  2. 如果这个日期已经过了,就加一年
  3. 计算距离下次生日的天数
  4. 计算进度百分比(已经过了多少)

动画触发

    // 动画
    scaleAnim.setValue(0);
    progressAnim.setValue(0);
    Animated.parallel([
      Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
      Animated.timing(progressAnim, { toValue: progressToNext, duration: 1000, useNativeDriver: false }),
    ]).start();

    setResult({ years, months, days, totalDays, totalWeeks, totalMonths, totalHours, daysToNext, progressToNext });
  };

计算完成后同时启动两个动画:结果卡片弹出和进度条填充。进度条动画用 useNativeDriver: false,因为要动态改变宽度。

界面渲染:头部和输入

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
          <Text style={styles.headerIcon}>🎂</Text>
        </Animated.View>
        <Text style={styles.headerTitle}>年龄计算</Text>
        <Text style={styles.headerSubtitle}>精确计算您的年龄</Text>
      </View>

      <View style={styles.inputCard}>
        <Text style={styles.label}>出生日期</Text>
        <View style={styles.inputWrapper}>
          <Text style={styles.inputIcon}>📅</Text>
          <TextInput
            style={styles.input}
            value={birthday}
            onChangeText={setBirthday}
            placeholder="YYYY-MM-DD"
            placeholderTextColor="#666"
          />
        </View>
      </View>

      <TouchableOpacity style={styles.btn} onPress={calculate} activeOpacity={0.8}>
        <View style={styles.btnInner}>
          <Text style={styles.btnIcon}>✨</Text>
          <Text style={styles.btnText}>计算年龄</Text>
        </View>
      </TouchableOpacity>

输入框前面加了日历图标,让用户更容易理解输入内容。

鸿蒙 ArkTS 对比:输入组件

Column() {
  Text('出生日期')
    .fontSize(14)
    .fontColor('#888888')
  
  Row() {
    Text('📅')
      .fontSize(24)
      .margin({ right: 12 })
    
    TextInput({ placeholder: 'YYYY-MM-DD' })
      .layoutWeight(1)
      .fontSize(20)
      .fontColor(Color.White)
      .backgroundColor(Color.Transparent)
      .onChange((value: string) => {
        this.birthday = value
      })
  }
  .backgroundColor('#252550')
  .borderRadius(12)
  .padding({ left: 16, right: 16 })
}

ArkTS 中用 Row 组件实现图标和输入框的水平排列,layoutWeight(1) 让输入框占据剩余空间。

主要年龄显示

      {result && !result.error && (
        <Animated.View style={[styles.resultCard, { transform: [{ scale: scaleAnim }] }]}>
          <View style={styles.mainAge}>
            <View style={styles.ageCircle}>
              <Text style={styles.ageNumber}>{result.years}</Text>
              <Text style={styles.ageUnit}>岁</Text>
            </View>
            <Text style={styles.ageDetail}>{result.months}个月 {result.days}天</Text>
          </View>

主要年龄用大圆圈显示,年数是最醒目的信息。下面补充显示月和日,给出完整的精确年龄。

进度条显示

          <View style={styles.progressSection}>
            <Text style={styles.progressLabel}>距离下次生日还有 {result.daysToNext} 天</Text>
            <View style={styles.progressBar}>
              <Animated.View style={[styles.progressFill, {
                width: progressAnim.interpolate({ inputRange: [0, 100], outputRange: ['0%', '100%'] })
              }]} />
            </View>
          </View>

进度条显示距离下次生日的进度,用 interpolate 把 0-100 的数值映射成 0%-100% 的宽度。动画效果让进度条从左到右填充。

统计数据网格

          <View style={styles.statsGrid}>
            {[
              { icon: '📆', value: result.totalDays.toLocaleString(), label: '天' },
              { icon: '📅', value: result.totalWeeks.toLocaleString(), label: '周' },
              { icon: '🗓️', value: result.totalMonths, label: '月' },
              { icon: '⏰', value: result.totalHours.toLocaleString(), label: '小时' },
            ].map((item, i) => (
              <View key={i} style={styles.statItem}>
                <Text style={styles.statIcon}>{item.icon}</Text>
                <Text style={styles.statValue}>{item.value}</Text>
                <Text style={styles.statLabel}>{item.label}</Text>
              </View>
            ))}
          </View>
        </Animated.View>
      )}

统计数据用四宫格显示,每个格子包含图标、数值和标签。toLocaleString() 给大数字添加千位分隔符。

错误提示

      {result?.error && (
        <View style={styles.errorBox}>
          <Text style={styles.errorText}>⚠️ {result.error}</Text>
        </View>
      )}
    </ScrollView>
  );
};

日期格式无效时显示错误提示。

样式定义:容器和输入

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
  header: { alignItems: 'center', marginBottom: 24 },
  headerIcon: { fontSize: 50, marginBottom: 8 },
  headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
  headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
  inputCard: {
    backgroundColor: '#1a1a3e',
    borderRadius: 20,
    padding: 20,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  label: { color: '#888', fontSize: 14, marginBottom: 12 },
  inputWrapper: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#252550',
    borderRadius: 12,
    paddingHorizontal: 16,
  },
  inputIcon: { fontSize: 24, marginRight: 12 },
  input: { flex: 1, fontSize: 20, color: '#fff', paddingVertical: 14 },

输入框用 flexDirection: 'row' 实现图标和文本框的水平排列。

按钮和结果样式

  btn: {
    backgroundColor: '#4A90D9',
    borderRadius: 16,
    marginBottom: 24,
    shadowColor: '#4A90D9',
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.4,
    shadowRadius: 15,
  },
  btnInner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 18 },
  btnIcon: { fontSize: 20, marginRight: 10 },
  btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
  resultCard: {
    backgroundColor: '#1a1a3e',
    borderRadius: 20,
    padding: 24,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  mainAge: { alignItems: 'center', marginBottom: 24 },
  ageCircle: {
    width: 140,
    height: 140,
    borderRadius: 70,
    backgroundColor: '#252550',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 4,
    borderColor: '#4A90D9',
    marginBottom: 12,
  },
  ageNumber: { fontSize: 48, fontWeight: '700', color: '#4A90D9' },
  ageUnit: { fontSize: 18, color: '#888' },
  ageDetail: { fontSize: 18, color: '#fff' },

年龄圆圈用蓝色边框,数字也用蓝色,形成视觉统一。

进度条和统计样式

  progressSection: { marginBottom: 24 },
  progressLabel: { color: '#888', fontSize: 14, marginBottom: 10, textAlign: 'center' },
  progressBar: {
    height: 8,
    backgroundColor: '#252550',
    borderRadius: 4,
    overflow: 'hidden',
  },
  progressFill: { height: '100%', backgroundColor: '#4A90D9', borderRadius: 4 },
  statsGrid: { flexDirection: 'row', flexWrap: 'wrap' },
  statItem: {
    width: '48%',
    backgroundColor: '#252550',
    borderRadius: 16,
    padding: 16,
    margin: '1%',
    alignItems: 'center',
  },
  statIcon: { fontSize: 24, marginBottom: 8 },
  statValue: { fontSize: 20, fontWeight: '700', color: '#fff' },
  statLabel: { fontSize: 12, color: '#888', marginTop: 4 },
  errorBox: {
    backgroundColor: 'rgba(231, 76, 60, 0.2)',
    borderRadius: 12,
    padding: 16,
    borderWidth: 1,
    borderColor: '#e74c3c',
  },
  errorText: { color: '#e74c3c', textAlign: 'center' },
});

进度条用 overflow: 'hidden' 确保填充部分不会超出容器。统计网格用 width: '48%' 实现两列布局。

小结

这个年龄计算器展示了 JavaScript 日期处理的一些技巧,特别是年龄计算中的借位处理。进度条动画让结果展示更有趣,统计数据让用户从不同角度了解自己的年龄。

在 OpenHarmony 平台上,日期计算和动画效果都能正常工作。JavaScript 的 Date 对象是跨平台的,不需要针对鸿蒙做特殊处理。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐