x
一、应用概述
1.1 应用简介
年龄计算器(Age Calculator)是一款精确的生命时间计算工具,能够根据用户输入的出生日期,精确计算到秒的年龄信息,同时提供星座查询、生肖查询、人生进度展示和纪念日提醒等增值功能。该应用以HarmonyOS NEXT的ArkTS框架为基础,深入展示了日期时间计算算法、星座算法、生肖算法、人生进度可视化和本地化日期处理等关键技术。
1.2 核心功能
| 功能模块 | 功能描述 | 技术实现 | 精度要求 |
|---|---|---|---|
| 精确年龄计算 | 计算到年/月/日/时/分/秒 | Date对象时间戳差值 | 毫秒级精度 |
| 总计时长统计 | 总天数/总小时/总分钟/总秒数 | 时间戳计算 | 高精度 |
| 星座查询 | 根据生日自动识别星座 | 日期边界比较算法 | 精确到日 |
| 生肖查询 | 计算中国农历生肖 | 年份模12运算 | 每年2月为界 |
| 人生进度 | 以80岁为标准展示生命进度 | 百分比计算 | 百分比可视化 |
| 下次生日倒计时 | 距离下次生日的精确时间 | 日期差计算 | 跨年处理 |
| 纪念日管理 | 自定义纪念日提醒 | 日期比较与定时器 | 精确到日 |
| 生辰八字 | 天干地支计算 | 干支纪年算法 | 传统历法 |
1.3 应用架构
年龄计算器采用三层架构设计:
- UI表现层:使用ArkTS声明式UI构建,包含日期选择器、年龄展示面板、星座生肖展示区、人生进度条和纪念日列表。
- 业务逻辑层:封装日期计算引擎、星座算法引擎、生肖干支算法和纪念日管理器。
- 数据持久层:使用Preferences API保存用户生日信息和纪念日记录。
二、日期时间计算算法
2.1 时间戳基础
JavaScript/TypeScript 中的时间计算基于 Unix 时间戳------自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。Date 对象的所有操作最终都归结为对时间戳的数学运算:
typescript
// 时间戳基本单位
const MS_PER_SECOND = 1000;
const MS_PER_MINUTE = 60 * MS_PER_SECOND;
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
const MS_PER_DAY = 24 * MS_PER_HOUR;
const MS_PER_WEEK = 7 * MS_PER_DAY;
const MS_PER_YEAR = 365.25 * MS_PER_DAY; // 考虑闰年
2.2 精确年龄计算
年龄计算的核心挑战在于:准确考虑闰年、月份天数差异和时区影响。我们的算法采用逐层精确计算:
typescript
interface AgeResult {
years: number; // 完整年数
months: number; // 剩余月数(0-11)
days: number; // 剩余天数(0-30)
hours: number; // 剩余小时数(0-23)
minutes: number; // 剩余分钟数(0-59)
seconds: number; // 剩余秒数(0-59)
totalDays: number; // 总天数
totalHours: number; // 总小时数
totalMinutes: number; // 总分钟数
totalSeconds: number; // 总秒数
isBirthday: boolean; // 今天是否是生日
nextBirthday: NextBirthdayInfo; // 下次生日信息
}
class AgeCalculator {
calculateAge(birthDate: Date): AgeResult {
const now = new Date();
const birth = new Date(birthDate);
// 计算时间戳差值
const diffMs = now.getTime() - birth.getTime();
// 计算总计时长
const totalDays = Math.floor(diffMs / MS_PER_DAY);
const totalHours = Math.floor(diffMs / MS_PER_HOUR);
const totalMinutes = Math.floor(diffMs / MS_PER_MINUTE);
const totalSeconds = Math.floor(diffMs / MS_PER_SECOND);
// 精确计算年、月、日(逐层计算)
let years = now.getFullYear() - birth.getFullYear();
let months = now.getMonth() - birth.getMonth();
let days = now.getDate() - birth.getDate();
// 调整负数天数
if (days < 0) {
months--;
// 获取上个月的天数
const lastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
days += lastMonth.getDate();
}
// 调整负数月份
if (months < 0) {
years--;
months += 12;
}
// 计算剩余小时、分钟、秒
const hours = now.getHours() - birth.getHours();
const minutes = now.getMinutes() - birth.getMinutes();
const seconds = now.getSeconds() - birth.getSeconds();
// 处理负数时间
// (简化处理,更精确的应逐层借位)
// 检查今天是否是生日
const isBirthday = now.getMonth() === birth.getMonth() &&
now.getDate() === birth.getDate();
// 计算下次生日
const nextBirthday = this.calculateNextBirthday(birth);
return {
years, months, days, hours, minutes, seconds,
totalDays, totalHours, totalMinutes, totalSeconds,
isBirthday, nextBirthday
};
}
}
2.3 精确月份与天数计算
年龄计算中最容易出错的是月份和天数的计算。我们设计了一套更精确的逐层借位算法:
typescript
// 精确年龄计算(逐层借位法)
calculateExactAge(birthDate: Date): AgeResult {
const now = new Date();
const birth = new Date(birthDate);
// 获取各时间分量
let birthYear = birth.getFullYear();
let birthMonth = birth.getMonth() + 1; // getMonth() 返回 0-11
let birthDay = birth.getDate();
let birthHour = birth.getHours();
let birthMinute = birth.getMinutes();
let birthSecond = birth.getSeconds();
let nowYear = now.getFullYear();
let nowMonth = now.getMonth() + 1;
let nowDay = now.getDate();
let nowHour = now.getHours();
let nowMinute = now.getMinutes();
let nowSecond = now.getSeconds();
// 秒借位
if (nowSecond < birthSecond) {
nowMinute--;
nowSecond += 60;
}
let seconds = nowSecond - birthSecond;
// 分钟借位
if (nowMinute < birthMinute) {
nowHour--;
nowMinute += 60;
}
let minutes = nowMinute - birthMinute;
// 小时借位
if (nowHour < birthHour) {
nowDay--;
nowHour += 24;
}
let hours = nowHour - birthHour;
// 天借位
if (nowDay < birthDay) {
nowMonth--;
// 获取上个月的天数
nowDay += this.getDaysInMonth(nowYear, nowMonth);
}
let days = nowDay - birthDay;
// 月借位
if (nowMonth < birthMonth) {
nowYear--;
nowMonth += 12;
}
let months = nowMonth - birthMonth;
let years = nowYear - birthYear;
// 计算总计时长
const diffMs = now.getTime() - birth.getTime();
const totalDays = Math.floor(diffMs / MS_PER_DAY);
const totalHours = Math.floor(diffMs / MS_PER_HOUR);
const totalMinutes = Math.floor(diffMs / MS_PER_MINUTE);
const totalSeconds = Math.floor(diffMs / MS_PER_SECOND);
const isBirthday = now.getMonth() === birth.getMonth() &&
now.getDate() === birth.getDate();
const nextBirthday = this.calculateNextBirthday(birth);
return {
years, months, days, hours, minutes, seconds,
totalDays, totalHours, totalMinutes, totalSeconds,
isBirthday, nextBirthday
};
}
// 获取某月天数
private getDaysInMonth(year: number, month: number): number {
// month 参数为 1-12
return new Date(year, month, 0).getDate();
}
// 判断闰年
private isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
2.4 下次生日计算
下次生日计算需要考虑跨年情况和闰年2月29日生日的特殊处理:
typescript
interface NextBirthdayInfo {
date: Date; // 下次生日日期
daysUntil: number; // 剩余天数
hoursUntil: number; // 剩余小时
minutesUntil: number; // 剩余分钟
secondsUntil: number; // 剩余秒数
totalDaysUntil: number; // 总剩余天数
}
calculateNextBirthday(birth: Date): NextBirthdayInfo {
const now = new Date();
const birthMonth = birth.getMonth();
const birthDay = birth.getDate();
// 构建今年生日
let nextBirthday = new Date(now.getFullYear(), birthMonth, birthDay);
// 如果今年生日已过,计算明年生日
// 特殊处理:如果生日是2月29日,非闰年时取2月28日
if (nextBirthday.getTime() <= now.getTime()) {
let nextYear = now.getFullYear() + 1;
// 如果生日是2月29日且明年不是闰年
if (birthMonth === 1 && birthDay === 29 && !this.isLeapYear(nextYear)) {
nextBirthday = new Date(nextYear, 1, 28);
} else {
nextBirthday = new Date(nextYear, birthMonth, birthDay);
}
}
const diffMs = nextBirthday.getTime() - now.getTime();
const totalDaysUntil = Math.floor(diffMs / MS_PER_DAY);
const hoursUntil = Math.floor((diffMs % MS_PER_DAY) / MS_PER_HOUR);
const minutesUntil = Math.floor((diffMs % MS_PER_HOUR) / MS_PER_MINUTE);
const secondsUntil = Math.floor((diffMs % MS_PER_MINUTE) / MS_PER_SECOND);
return {
date: nextBirthday,
daysUntil: totalDaysUntil,
hoursUntil, minutesUntil, secondsUntil,
totalDaysUntil
};
}
三、星座算法
3.1 星座日期边界
星座是根据公历日期划分的,每个星座对应一个特定的日期区间。星座算法的核心是日期边界比较:
typescript
interface ZodiacSign {
name: string; // 星座名称
englishName: string; // 英文名称
symbol: string; // 星座符号
emoji: string; // 表情符号
startMonth: number; // 开始月份 (1-12)
startDay: number; // 开始日期 (1-31)
endMonth: number; // 结束月份
endDay: number; // 结束日期
element: string; // 星座元素(火/土/风/水)
quality: string; // 星座特质(基本/固定/变动)
ruler: string; // 守护星
luckyNumber: number; // 幸运数字
}
class ZodiacSystem {
private static readonly SIGNS: ZodiacSign[] = [
{ name: '白羊座', englishName: 'Aries', symbol: '♈', emoji: '🐏',
startMonth: 3, startDay: 21, endMonth: 4, endDay: 19,
element: '火', quality: '基本', ruler: '火星', luckyNumber: 1 },
{ name: '金牛座', englishName: 'Taurus', symbol: '♉', emoji: '🐂',
startMonth: 4, startDay: 20, endMonth: 5, endDay: 20,
element: '土', quality: '固定', ruler: '金星', luckyNumber: 6 },
{ name: '双子座', englishName: 'Gemini', symbol: '♊', emoji: '👯',
startMonth: 5, startDay: 21, endMonth: 6, endDay: 21,
element: '风', quality: '变动', ruler: '水星', luckyNumber: 5 },
{ name: '巨蟹座', englishName: 'Cancer', symbol: '♋', emoji: '🦀',
startMonth: 6, startDay: 22, endMonth: 7, endDay: 22,
element: '水', quality: '基本', ruler: '月亮', luckyNumber: 2 },
{ name: '狮子座', englishName: 'Leo', symbol: '♌', emoji: '🦁',
startMonth: 7, startDay: 23, endMonth: 8, endDay: 22,
element: '火', quality: '固定', ruler: '太阳', luckyNumber: 1 },
{ name: '处女座', englishName: 'Virgo', symbol: '♍', emoji: '👩',
startMonth: 8, startDay: 23, endMonth: 9, endDay: 22,
element: '土', quality: '变动', ruler: '水星', luckyNumber: 5 },
{ name: '天秤座', englishName: 'Libra', symbol: '♎', emoji: '⚖️',
startMonth: 9, startDay: 23, endMonth: 10, endDay: 23,
element: '风', quality: '基本', ruler: '金星', luckyNumber: 6 },
{ name: '天蝎座', englishName: 'Scorpio', symbol: '♏', emoji: '🦂',
startMonth: 10, startDay: 24, endMonth: 11, endDay: 22,
element: '水', quality: '固定', ruler: '冥王星', luckyNumber: 9 },
{ name: '射手座', englishName: 'Sagittarius', symbol: '♐', emoji: '🏹',
startMonth: 11, startDay: 23, endMonth: 12, endDay: 21,
element: '火', quality: '变动', ruler: '木星', luckyNumber: 3 },
{ name: '摩羯座', englishName: 'Capricorn', symbol: '♑', emoji: '🐐',
startMonth: 12, startDay: 22, endMonth: 1, endDay: 19,
element: '土', quality: '基本', ruler: '土星', luckyNumber: 8 },
{ name: '水瓶座', englishName: 'Aquarius', symbol: '♒', emoji: '🏺',
startMonth: 1, startDay: 20, endMonth: 2, endDay: 18,
element: '风', quality: '固定', ruler: '天王星', luckyNumber: 4 },
{ name: '双鱼座', englishName: 'Pisces', symbol: '♓', emoji: '🐟',
startMonth: 2, startDay: 19, endMonth: 3, endDay: 20,
element: '水', quality: '变动', ruler: '海王星', luckyNumber: 7 }
];
// 获取星座信息
getZodiacSign(month: number, day: number): ZodiacSign | null {
// 转换为日期序号(一年中的第几天)
const dateNum = month * 100 + day;
for (const sign of this.SIGNS) {
const startNum = sign.startMonth * 100 + sign.startDay;
const endNum = sign.endMonth * 100 + sign.endDay;
// 处理跨年情况(摩羯座 12/22 - 1/19)
if (startNum <= endNum) {
if (dateNum >= startNum && dateNum <= endNum) {
return sign;
}
} else {
// 跨年:dateNum >= startNum 或 dateNum <= endNum
if (dateNum >= startNum || dateNum <= endNum) {
return sign;
}
}
}
return null;
}
// 替代实现:使用边界数组(更简洁)
getZodiacSignSimple(month: number, day: number): string {
const boundaries = [20, 19, 21, 20, 21, 21, 23, 23, 23, 23, 22, 22];
const index = (day < boundaries[month - 1]) ? month - 1 : month % 12;
return this.SIGNS[index].name;
}
}
3.2 星座匹配度分析
作为趣味功能,我们添加了星座匹配度分析:
typescript
interface ZodiacMatch {
compatibility: number; // 匹配度 0-100
description: string; // 匹配描述
strengths: string[]; // 优势
challenges: string[]; // 挑战
}
class ZodiacMatchMaker {
// 星座元素兼容性矩阵
private static readonly ELEMENT_COMPATIBILITY: Record<string, Record<string, number>> = {
'火': { '火': 70, '土': 50, '风': 85, '水': 60 },
'土': { '火': 50, '土': 75, '风': 55, '水': 80 },
'风': { '火': 85, '土': 55, '风': 65, '水': 50 },
'水': { '火': 60, '土': 80, '风': 50, '水': 70 }
};
calculateMatch(zodiac1: ZodiacSign, zodiac2: ZodiacSign): ZodiacMatch {
// 基础分:元素兼容性
const elementScore = this.ELEMENT_COMPATIBILITY[zodiac1.element][zodiac2.element];
// 特质匹配
let qualityScore = 50;
if (zodiac1.quality === zodiac2.quality) {
qualityScore = 70; // 同特质更理解对方
}
// 综合得分
const compatibility = Math.round((elementScore + qualityScore) / 2);
let description = '';
if (compatibility >= 80) {
description = `${zodiac1.name}和${zodiac2.name}是天生一对!`;
} else if (compatibility >= 60) {
description = `${zodiac1.name}和${zodiac2.name}相处融洽。`;
} else {
description = `${zodiac1.name}和${zodiac2.name}需要更多包容。`;
}
return {
compatibility,
description,
strengths: ['相互理解', '精神共鸣'],
challenges: ['性格差异']
};
}
}
四、生肖算法
4.1 中国生肖计算
中国生肖(十二生肖)以农历年份为基准,但公历年份的生肖计算以春节(立春)为界:
typescript
interface ChineseZodiac {
name: string; // 生肖名称
emoji: string; // 表情符号
element: string; // 五行属性
yinYang: '阴' | '阳'; // 阴阳
trine: number; // 三合(与其他生肖的和谐关系)
clash: string; // 相冲生肖
luckyDirections: string[]; // 吉利方位
luckyColors: string[]; // 吉利颜色
luckyNumbers: number[]; // 吉利数字
}
class ChineseZodiacCalculator {
private static readonly ZODIACS: ChineseZodiac[] = [
{ name: '鼠', emoji: '🐭', element: '水', yinYang: '阳', trine: 0, clash: '马',
luckyDirections: ['东北', '西南'], luckyColors: ['蓝色', '金色'], luckyNumbers: [2, 3] },
{ name: '牛', emoji: '🐮', element: '土', yinYang: '阴', trine: 1, clash: '羊',
luckyDirections: ['东北', '正北'], luckyColors: ['黄色', '红色'], luckyNumbers: [1, 9] },
{ name: '虎', emoji: '🐯', element: '木', yinYang: '阳', trine: 2, clash: '猴',
luckyDirections: ['正南', '西北'], luckyColors: ['绿色', '蓝色'], luckyNumbers: [3, 4] },
{ name: '兔', emoji: '🐰', element: '木', yinYang: '阴', trine: 0, clash: '鸡',
luckyDirections: ['正东', '西南'], luckyColors: ['红色', '粉色'], luckyNumbers: [3, 6] },
{ name: '龙', emoji: '🐲', element: '土', yinYang: '阳', trine: 1, clash: '狗',
luckyDirections: ['正西', '西北'], luckyColors: ['金色', '银色'], luckyNumbers: [1, 7] },
{ name: '蛇', emoji: '🐍', element: '火', yinYang: '阴', trine: 2, clash: '猪',
luckyDirections: ['正南', '东南'], luckyColors: ['红色', '紫色'], luckyNumbers: [2, 8] },
{ name: '马', emoji: '🐴', element: '火', yinYang: '阳', trine: 0, clash: '鼠',
luckyDirections: ['东北', '西南'], luckyColors: ['绿色', '红色'], luckyNumbers: [3, 7] },
{ name: '羊', emoji: '🐏', element: '土', yinYang: '阴', trine: 1, clash: '牛',
luckyDirections: ['正南', '东南'], luckyColors: ['黄色', '红色'], luckyNumbers: [2, 7] },
{ name: '猴', emoji: '🐵', element: '金', yinYang: '阳', trine: 2, clash: '虎',
luckyDirections: ['西北', '西南'], luckyColors: ['白色', '金色'], luckyNumbers: [4, 9] },
{ name: '鸡', emoji: '🐔', element: '金', yinYang: '阴', trine: 0, clash: '兔',
luckyDirections: ['正西', '东北'], luckyColors: ['金色', '白色'], luckyNumbers: [5, 7] },
{ name: '狗', emoji: '🐶', element: '土', yinYang: '阳', trine: 1, clash: '龙',
luckyDirections: ['正南', '西北'], luckyColors: ['红色', '黄色'], luckyNumbers: [3, 7] },
{ name: '猪', emoji: '🐷', element: '水', yinYang: '阴', trine: 2, clash: '蛇',
luckyDirections: ['东北', '西南'], luckyColors: ['黑色', '蓝色'], luckyNumbers: [2, 5] }
];
// 生肖列表(从鼠开始,与年份模12对应)
private static readonly ZODIAC_NAMES = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
// 计算生肖(以春节为界)
getChineseZodiac(year: number, month?: number, day?: number): ChineseZodiac {
// 如果提供了月日,考虑春节因素
if (month !== undefined && day !== undefined) {
const springFestival = this.getSpringFestivalDate(year);
// 如果在春节前,算前一年的生肖
const birthDate = new Date(year, month - 1, day);
if (birthDate < springFestival) {
year--;
}
}
// 生肖以 4 年(鼠年)为基准
// 2020 = 鼠年 => 2020 % 12 = 4
// 索引 = (year - 4) % 12
const index = ((year - 4) % 12 + 12) % 12;
return this.ZODIACS[index];
}
// 获取生肖名称(简单版)
getZodiacName(year: number): string {
const zodiacs = ['猴', '鸡', '狗', '猪', '鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊'];
return zodiacs[year % 12];
}
// 获取春节日期(近似值,精确值需要农历库)
private getSpringFestivalDate(year: number): Date {
// 春节一般在1月21日到2月20日之间
// 这里使用简化的近似算法
const springFestivalMonths: Record<number, { month: number; day: number }> = {
2023: { month: 1, day: 22 },
2024: { month: 2, day: 10 },
2025: { month: 1, day: 29 },
2026: { month: 2, day: 17 },
2027: { month: 2, day: 6 },
2028: { month: 1, day: 26 }
};
const sf = springFestivalMonths[year];
if (sf) {
return new Date(year, sf.month - 1, sf.day);
}
// 默认使用2月1日作为近似值
return new Date(year, 1, 1);
}
}
4.2 天干地支(生辰八字)计算
天干地支是中国传统历法的重要组成部分,用于记年、月、日、时:
typescript
interface BaZi {
yearPillar: string; // 年柱
monthPillar: string; // 月柱
dayPillar: string; // 日柱
hourPillar: string; // 时柱
fourPillars: string; // 四柱八字
}
class BaZiCalculator {
private static readonly HEAVENLY_STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
private static readonly EARTHLY_BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
// 计算年柱
calculateYearPillar(year: number): { stem: string; branch: string } {
// 天干索引 = (year - 4) % 10
// 地支索引 = (year - 4) % 12
const stemIndex = ((year - 4) % 10 + 10) % 10;
const branchIndex = ((year - 4) % 12 + 12) % 12;
return {
stem: this.HEAVENLY_STEMS[stemIndex],
branch: this.EARTHLY_BRANCHES[branchIndex]
};
}
// 计算时柱
calculateHourPillar(hour: number): { stem: string; branch: string } {
// 时辰地支:每2小时一个时辰
// 23:00-00:59 子时, 01:00-02:59 丑时, ...
const branchIndex = Math.floor((hour + 1) / 2) % 12;
// 时干根据日干推算(五鼠遁法)
// 甲己日 -> 甲子时, 乙庚日 -> 丙子时, ...
const stemIndex = 0; // 简化处理
return {
stem: this.HEAVENLY_STEMS[stemIndex],
branch: this.EARTHLY_BRANCHES[branchIndex]
};
}
// 计算完整八字
calculateBaZi(year: number, month: number, day: number, hour: number): BaZi {
const yearPillar = this.calculateYearPillar(year);
const monthPillar = this.calculateMonthPillar(year, month);
const dayPillar = this.calculateDayPillar(year, month, day);
const hourPillar = this.calculateHourPillar(hour);
return {
yearPillar: `${yearPillar.stem}${yearPillar.branch}`,
monthPillar: `${monthPillar.stem}${monthPillar.branch}`,
dayPillar: `${dayPillar.stem}${dayPillar.branch}`,
hourPillar: `${hourPillar.stem}${hourPillar.branch}`,
fourPillars: `${yearPillar.stem}${yearPillar.branch} ${monthPillar.stem}${monthPillar.branch} ${dayPillar.stem}${dayPillar.branch} ${hourPillar.stem}${hourPillar.branch}`
};
}
}
五、人生进度可视化
5.1 人生进度模型
以80岁为人类平均寿命基准,计算生命进度:
typescript
interface LifeProgress {
livedYears: number; // 已活年数
totalYears: number; // 预期寿命
percentage: number; // 已完成百分比
remainingYears: number; // 剩余年数
remainingDays: number; // 剩余天数
livedMoments: string; // 已活时间的文字描述
milestones: Milestone[]; // 人生里程碑
season: string; // 人生季节(春/夏/秋/冬)
}
interface Milestone {
age: number;
title: string;
description: string;
isReached: boolean;
icon: string;
}
class LifeProgressCalculator {
private static readonly EXPECTED_LIFESPAN = 80;
private static readonly MILESTONES: Milestone[] = [
{ age: 0, title: '诞生', description: '来到这个世界', icon: '👶', isReached: true },
{ age: 6, title: '入学', description: '开始学习生涯', icon: '📚', isReached: false },
{ age: 18, title: '成年', description: '迈向成年', icon: '🎓', isReached: false },
{ age: 22, title: '大学毕业', description: '步入社会', icon: '🎓', isReached: false },
{ age: 30, title: '而立之年', description: '事业稳定', icon: '💼', isReached: false },
{ age: 40, title: '不惑之年', description: '人生沉淀', icon: '🧘', isReached: false },
{ age: 50, title: '知天命', description: '豁达从容', icon: '🌟', isReached: false },
{ age: 60, title: '花甲之年', description: '退休生活', icon: '🌺', isReached: false },
{ age: 70, title: '古稀之年', description: '安享晚年', icon: '🌅', isReached: false },
{ age: 80, title: '耄耋之年', description: '长寿安康', icon: '🎊', isReached: false }
];
calculate(birthDate: Date): LifeProgress {
const now = new Date();
const ageMs = now.getTime() - birthDate.getTime();
const ageYears = ageMs / MS_PER_YEAR;
const livedYears = Math.round(ageYears * 10) / 10;
const percentage = Math.min(100, Math.round(ageYears / this.EXPECTED_LIFESPAN * 1000) / 10);
const remainingYears = Math.round((this.EXPECTED_LIFESPAN - ageYears) * 10) / 10;
const remainingDays = Math.round(remainingYears * 365.25);
const season = this.getLifeSeason(livedYears);
const milestones = this.MILESTONES.map(m => ({
...m,
isReached: livedYears >= m.age
}));
return {
livedYears, totalYears: this.EXPECTED_LIFESPAN,
percentage, remainingYears, remainingDays,
livedMoments: this.formatLivedTime(ageMs),
milestones, season
};
}
private getLifeSeason(age: number): string {
if (age < 20) return '🌱 春天 --- 成长的季节';
if (age < 40) return '☀️ 夏天 --- 奋斗的季节';
if (age < 60) return '🍂 秋天 --- 收获的季节';
return '❄️ 冬天 --- 沉淀的季节';
}
private formatLivedTime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const years = Math.floor(days / 365.25);
return `你已经在这个世界上度过了 ${years} 年、${days % 365} 天、${hours % 24} 小时、${minutes % 60} 分钟、${seconds % 60} 秒`;
}
}
5.2 进度环组件
typescript
@Component
struct LifeProgressRing {
@Link progress: LifeProgress;
build() {
Column() {
// 进度环
Stack() {
// 背景环
Circle()
.width(200)
.height(200)
.fill('none')
.stroke('#E0E0E0')
.strokeWidth(12)
// 进度环
Circle()
.width(200)
.height(200)
.fill('none')
.stroke('#4CAF50')
.strokeWidth(12)
.strokeDashOffset(this.progress.percentage / 100 * 565.48)
.rotate({ angle: -90 })
// 中心文字
Column() {
Text(`${this.progress.percentage}%`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Text('生命进度')
.fontSize(14)
.fontColor('#666666')
}
}
.width(200)
.height(200)
.margin({ bottom: 16 })
// 季节文字
Text(this.progress.season)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ bottom: 8 })
// 剩余时间
Text(`剩余约 ${this.progress.remainingYears} 年`)
.fontSize(16)
.fontColor('#666666')
// 里程碑列表
List({ space: 8 }) {
ForEach(this.progress.milestones, (milestone: Milestone) => {
ListItem() {
Row() {
Text(milestone.icon)
.fontSize(24)
Column() {
Text(milestone.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
Text(milestone.description)
.fontSize(12)
.fontColor('#666666')
}
.margin({ left: 12 })
if (milestone.isReached) {
Text('✅ 已达成')
.fontSize(12)
.fontColor('#4CAF50')
}
}
.width('100%')
.padding(12)
.backgroundColor(milestone.isReached ? '#E8F5E9' : '#F5F5F5')
.borderRadius(8)
}
})
}
.width('100%')
.height('40%')
}
.width('100%')
.padding(16)
}
}
六、纪念日管理
6.1 纪念日数据模型
typescript
interface Anniversary {
id: string;
name: string; // 纪念日名称
date: Date; // 纪念日日期
type: 'birthday' | 'custom' | 'holiday';
remindBefore: number; // 提前提醒天数
repeat: boolean; // 是否每年重复
notes: string; // 备注
color: string; // 显示颜色
icon: string; // 图标
}
class AnniversaryManager {
private anniversaries: Anniversary[] = [];
private prefs: preferences.Preferences | null = null;
async init(context: Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, 'anniversary_prefs');
await this.load();
}
// 计算纪念日倒计时
calculateCountdown(anniversary: Anniversary): { days: number; status: 'today' | 'upcoming' | 'past' } {
const now = new Date();
const targetDate = new Date(anniversary.date);
if (anniversary.repeat) {
// 每年重复的纪念日
targetDate.setFullYear(now.getFullYear());
if (targetDate < now) {
targetDate.setFullYear(now.getFullYear() + 1);
}
}
const diffMs = targetDate.getTime() - now.getTime();
const days = Math.ceil(diffMs / MS_PER_DAY);
let status: 'today' | 'upcoming' | 'past';
if (days === 0) status = 'today';
else if (days > 0) status = 'upcoming';
else status = 'past';
return { days, status };
}
// 获取即将到来的纪念日
getUpcomingAnniversaries(days: number = 30): Anniversary[] {
const now = new Date();
const future = new Date(now.getTime() + days * MS_PER_DAY);
return this.anniversaries.filter(a => {
const countdown = this.calculateCountdown(a);
return countdown.status !== 'past' && countdown.days <= days;
}).sort((a, b) => {
return this.calculateCountdown(a).days - this.calculateCountdown(b).days;
});
}
}
七、UI交互设计
7.1 日期选择器
typescript
@Component
struct BirthdayPicker {
@State selectedYear: number = 2000;
@State selectedMonth: number = 1;
@State selectedDay: number = 1;
@State showPicker: boolean = false;
private years: number[] = [];
private months: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
aboutToAppear(): void {
const currentYear = new Date().getFullYear();
for (let y = currentYear - 120; y <= currentYear; y++) {
this.years.push(y);
}
}
get daysInMonth(): number[] {
const days = new Date(this.selectedYear, this.selectedMonth, 0).getDate();
return Array.from({ length: days }, (_, i) => i + 1);
}
build() {
Column() {
Button(this.formatDate(this.selectedYear, this.selectedMonth, this.selectedDay))
.onClick(() => {
this.showPicker = !this.showPicker;
})
if (this.showPicker) {
Row() {
// 年份选择器
Picker({ range: this.years, selected: this.years.indexOf(this.selectedYear) })
.onChange((index: number) => {
this.selectedYear = this.years[index];
})
// 月份选择器
Picker({ range: this.months, selected: this.selectedMonth - 1 })
.onChange((index: number) => {
this.selectedMonth = this.months[index];
})
// 日期选择器
Picker({ range: this.daysInMonth, selected: this.selectedDay - 1 })
.onChange((index: number) => {
this.selectedDay = this.daysInMonth[index];
})
}
.width('100%')
.height(200)
}
}
}
private formatDate(year: number, month: number, day: number): string {
return `${year}年${month}月${day}日`;
}
}
八、总结
8.1 核心技术要点
- 逐层借位年龄计算:精确到秒的年龄计算,正确处理闰年、月份天数差异和时区问题。
- 星座算法:基于日期边界比较的星座识别,处理跨年星座(摩羯座)的特殊情况。
- 生肖与干支算法:以春节为界的生肖计算,以及天干地支(生辰八字)的完整推算算法。
- 人生进度模型:以80岁为基准的生命进度计算,包含里程碑节点和人生季节划分。
- 纪念日管理:重复纪念日的倒计时计算,提前提醒和智能排序功能。
8.2 扩展方向
- 农历日历集成:接入完整的农历日历库,提供更准确的节气、节日和生肖计算。
- 年龄运势分析:结合星座、生肖和八字提供个性化的运势分析。
- 家族树功能:记录家族成员的生辰信息,生成家族关系图谱。
- 健康寿命预测:基于统计学模型预测更个性化的预期寿命。
- 历史事件关联:将用户生日与历史上的同日事件关联,增加趣味性。
8.3 核心代码量统计
| 模块 | 核心代码行数 | 接口数 | 组件数 |
|---|---|---|---|
| 年龄计算引擎 | 180 | 6 | - |
| 星座算法系统 | 150 | 5 | - |
| 生肖干支算法 | 200 | 8 | - |
| 人生进度模型 | 120 | 4 | - |
| 纪念日管理器 | 140 | 6 | - |
| UI组件 | 320 | 6 | 9 |
| 总计 | 1110 | 35 | 9 |