如果你想做一个宠物龟记录App,需要用到Preferences和倒计时
欢迎大家去鸿蒙应用市场搜索下载「龟寿录」,体验一下这款宠物龟记录工具。你可以在里面记录背甲测量数据、设置冬眠倒计时、管理喂食频率日志,把你养龟的每一个细节都记下来。
写在前面
养龟是一件"慢"事。龟的生长速度很慢,一年可能只长几厘米。但正因为慢,所以更需要长期记录。如果你能坚持每隔几个月测量一次背甲长度,积累一两年之后,就能画出一条完整的生长曲线,清楚地看到龟的生长趋势。
冬眠是养龟过程中最让人操心的环节。不同品种的龟,冬眠的温度和时间都不一样。在冬眠期间,你需要定期检查龟的状态,确保温度不会太低(会冻死)也不会太高(会提前醒来)。如果能有一个倒计时,告诉你距离冬眠结束还有多少天,你心里就有底了。
喂食频率也很重要。龟在活跃期需要定期喂食,但在冬眠前需要停食排空肠胃,冬眠期间完全停食。如果有一个喂食日志,你就能清楚地知道上次喂食是什么时候、喂了多少、龟的反应如何。
这就是「龟寿录」这个App要做的三件事:背甲测量、冬眠倒计时、喂食频率日志。
这篇文章聊什么
这篇文章主要聊三件事:
- 测量数据的时序存储 -- 怎么按时间顺序存储背甲长度、体重等测量数据,并计算生长速率。
- 冬眠倒计时的实现 -- 怎么根据冬眠开始日期和预计结束日期,计算剩余天数,并用动画效果展示倒计时。
- 喂食频率日志的管理 -- 怎么记录每次喂食的详细信息,并计算喂食间隔。
第一步:背甲测量数据记录
背甲长度(Carapace Length,简称CL)是衡量龟体型大小的标准指标。定期测量并记录,可以追踪龟的生长情况。
React版本:背甲测量
const defaultMeasurements = [
{ id: '1', date: '2024-01-15', carapaceLength: 12.5, weight: 350, note: '状态良好' },
{ id: '2', date: '2024-04-15', carapaceLength: 13.1, weight: 380, note: '食欲旺盛' },
{ id: '3', date: '2024-07-15', carapaceLength: 13.8, weight: 420, note: '进入活跃期' },
];
function MeasurementTracker() {
const [measurements, setMeasurements] = useState(() => {
const saved = localStorage.getItem('turtle_measurements');
return saved ? JSON.parse(saved) : defaultMeasurements;
});
const addMeasurement = (m) => {
const updated = [{ ...m, id: Date.now().toString() }, ...measurements];
setMeasurements(updated);
localStorage.setItem('turtle_measurements', JSON.stringify(updated));
};
// 计算生长速率(每月增长多少厘米)
const getGrowthRate = () => {
if (measurements.length < 2) return null;
const latest = measurements[0];
const oldest = measurements[measurements.length - 1];
const monthsDiff = (new Date(latest.date) - new Date(oldest.date)) / (1000 * 60 * 60 * 24 * 30);
const lengthDiff = latest.carapaceLength - oldest.carapaceLength;
return (lengthDiff / monthsDiff).toFixed(2);
};
return (
<div className="measurement-tracker">
<h3>背甲测量</h3>
<p>当前背甲长:{measurements[0]?.carapaceLength} cm</p>
<p>月均增长:{getGrowthRate()} cm/月</p>
{/* 测量列表 */}
</div>
);
}
生长速率的计算逻辑是:用最新一次的背甲长度减去最早一次的背甲长度,再除以两次测量之间的月数。这样得到的就是"每月平均增长多少厘米"。如果只有一次测量数据,就无法计算速率,所以返回null。
ArkTS版本:背甲测量
import { preferences } from '@kit.ArkData';
// 测量记录
interface Measurement {
id: string;
date: string;
carapaceLength: number; // 背甲长度,cm
weight: number; // 体重,g
note: string;
}
let dataStore: preferences.Preferences | null = null;
@Entry
@Component
struct MeasurementPage {
@State measurements: Measurement[] = [];
@State inputLength: number = 15.0;
@State inputWeight: number = 400;
@State inputNote: string = '';
async aboutToAppear() {
try {
dataStore = await preferences.getPreferences(getContext(this), 'guishoulu_store');
const json = await dataStore.get('turtle_measurements', '[]') as string;
this.measurements = JSON.parse(json) as Measurement[];
} catch (err) {
console.error('加载测量数据失败', JSON.stringify(err));
}
}
// 计算生长速率
getGrowthRate(): string {
if (this.measurements.length < 2) return '--';
const latest = this.measurements[0];
const oldest = this.measurements[this.measurements.length - 1];
const monthsDiff = (new Date(latest.date).getTime() - new Date(oldest.date).getTime()) / (1000 * 60 * 60 * 24 * 30);
if (monthsDiff === 0) return '--';
const lengthDiff = latest.carapaceLength - oldest.carapaceLength;
return (lengthDiff / monthsDiff).toFixed(2);
}
// 计算体重增长速率
getWeightGrowthRate(): string {
if (this.measurements.length < 2) return '--';
const latest = this.measurements[0];
const oldest = this.measurements[this.measurements.length - 1];
const monthsDiff = (new Date(latest.date).getTime() - new Date(oldest.date).getTime()) / (1000 * 60 * 60 * 24 * 30);
if (monthsDiff === 0) return '--';
const weightDiff = latest.weight - oldest.weight;
return (weightDiff / monthsDiff).toFixed(0);
}
async addMeasurement() {
const m: Measurement = {
id: Date.now().toString(),
date: new Date().toISOString().split('T')[0],
carapaceLength: this.inputLength,
weight: this.inputWeight,
note: this.inputNote,
};
this.measurements = [m, ...this.measurements];
if (dataStore) {
await dataStore.put('turtle_measurements', JSON.stringify(this.measurements));
await dataStore.flush();
}
this.inputNote = '';
}
build() {
Scroll() {
Column() {
Text('背甲测量记录')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 当前数据卡片
if (this.measurements.length > 0) {
Row({ space: 12 }) {
Column() {
Text(`${this.measurements[0].carapaceLength}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Text('背甲长度(cm)')
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
.padding(16)
.backgroundColor('#E8F5E9')
.borderRadius(12)
Column() {
Text(`${this.measurements[0].weight}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#2196F3')
Text('体重(g)')
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
.padding(16)
.backgroundColor('#E3F2FD')
.borderRadius(12)
}
.width('90%')
.margin({ bottom: 12 })
// 生长速率
Row({ space: 12 }) {
Column() {
Text(`${this.getGrowthRate()}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FF9800')
Text('月均增长(cm/月)')
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
.padding(12)
.backgroundColor('#FFF3E0')
.borderRadius(12)
Column() {
Text(`${this.getWeightGrowthRate()}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#9C27B0')
Text('月均增重(g/月)')
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
.padding(12)
.backgroundColor('#F3E5F5')
.borderRadius(12)
}
.width('90%')
.margin({ bottom: 20 })
}
// 添加测量
Column() {
Text('添加测量')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
Row() {
Text('背甲长度')
.fontSize(14)
.width(80)
Text(`${this.inputLength} cm`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.margin({ left: 8, right: 8 })
Slider({ min: 1, max: 50, step: 0.1, value: this.inputLength, style: SliderStyle.OutSet })
.layoutWeight(1)
.onChange((value: number) => { this.inputLength = value; })
}
.width('100%')
.margin({ bottom: 8 })
Row() {
Text('体重')
.fontSize(14)
.width(80)
Text(`${this.inputWeight} g`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.margin({ left: 8, right: 8 })
Slider({ min: 10, max: 5000, step: 10, value: this.inputWeight, style: SliderStyle.OutSet })
.layoutWeight(1)
.onChange((value: number) => { this.inputWeight = value; })
}
.width('100%')
.margin({ bottom: 8 })
TextArea({ placeholder: '备注...', text: this.inputNote })
.width('100%')
.height(50)
.onChange((value: string) => { this.inputNote = value; })
.margin({ bottom: 12 })
Button('记录测量')
.width('100%')
.backgroundColor('#4CAF50')
.fontColor('#FFFFFF')
.onClick(() => { this.addMeasurement(); })
}
.width('90%')
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.margin({ bottom: 20 })
// 测量历史
Text('测量历史')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('90%')
.margin({ bottom: 12 })
ForEach(this.measurements, (m: Measurement) => {
Row() {
Text(m.date)
.fontSize(13)
.fontColor('#999999')
.width(90)
Text(`${m.carapaceLength}cm`)
.fontSize(13)
.fontColor('#4CAF50')
.fontWeight(FontWeight.Bold)
.width(60)
Text(`${m.weight}g`)
.fontSize(13)
.fontColor('#2196F3')
.width(60)
if (m.note) {
Text(m.note)
.fontSize(12)
.fontColor('#666666')
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
.width('90%')
.padding(10)
.backgroundColor('#FAFAFA')
.borderRadius(6)
.margin({ bottom: 4 })
}, (m: Measurement) => m.id)
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
}
}
测量历史列表里,每条记录显示日期、背甲长度、体重和备注。背甲长度用绿色显示,体重用蓝色显示,这样视觉上更容易区分。备注如果太长,用maxLines(1)和textOverflow来截断,避免占用太多空间。
第二步:冬眠倒计时
冬眠倒计时是这个App最有特色的功能。用户设置冬眠的开始日期和预计结束日期,App会自动计算剩余天数,并用醒目的方式展示。
React版本:冬眠倒计时
function HibernationCountdown() {
const [config, setConfig] = useState(() => {
const saved = localStorage.getItem('hibernation_config');
return saved ? JSON.parse(saved) : {
startDate: '2024-11-15',
endDate: '2025-03-15',
isHibernating: false,
};
});
const [now, setNow] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 60000); // 每分钟更新
return () => clearInterval(timer);
}, []);
const getDaysRemaining = () => {
const end = new Date(config.endDate);
const diff = end - now;
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
};
const getDaysElapsed = () => {
const start = new Date(config.startDate);
const diff = now - start;
return Math.max(0, Math.floor(diff / (1000 * 60 * 60 * 24)));
};
const startHibernation = () => {
const updated = { ...config, isHibernating: true, startDate: new Date().toISOString().split('T')[0] };
setConfig(updated);
localStorage.setItem('hibernation_config', JSON.stringify(updated));
};
const endHibernation = () => {
const updated = { ...config, isHibernating: false };
setConfig(updated);
localStorage.setItem('hibernation_config', JSON.stringify(updated));
};
return (
<div className="hibernation-countdown">
<h3>冬眠倒计时</h3>
{config.isHibernating ? (
<div className="countdown-display">
<p>已冬眠 {getDaysElapsed()} 天</p>
<p>距离结束还有 {getDaysRemaining()} 天</p>
<button onClick={endHibernation}>结束冬眠</button>
</div>
) : (
<div>
<p>当前不在冬眠期</p>
<button onClick={startHibernation}>开始冬眠</button>
</div>
)}
</div>
);
}
倒计时的核心逻辑是计算当前时间与结束日期之间的天数差。用setInterval每分钟更新一次当前时间,这样倒计时就能实时更新。不过在实际的App中,你可以用更精确的定时器(比如每秒更新),但每分钟更新已经足够了,毕竟天数级别的倒计时不需要秒级精度。
ArkTS版本:冬眠倒计时
interface HibernationConfig {
startDate: string;
endDate: string;
isHibernating: boolean;
targetTemp: number; // 冬眠目标温度
}
@Entry
@Component
struct HibernationPage {
@State config: HibernationConfig = {
startDate: '',
endDate: '',
isHibernating: false,
targetTemp: 5,
};
@State daysRemaining: number = 0;
@State daysElapsed: number = 0;
@State editEndDate: string = '';
@State editTargetTemp: number = 5;
private timerId: number = -1;
async aboutToAppear() {
if (!dataStore) return;
try {
const json = await dataStore.get('hibernation_config', '') as string;
if (json !== '') {
this.config = JSON.parse(json) as HibernationConfig;
}
} catch (err) {
console.error('加载冬眠配置失败', JSON.stringify(err));
}
this.updateCountdown();
// 每分钟更新一次倒计时
this.timerId = setInterval(() => {
this.updateCountdown();
}, 60000);
}
aboutToDisappear() {
if (this.timerId !== -1) {
clearInterval(this.timerId);
}
}
// 更新倒计时
updateCountdown() {
if (!this.config.isHibernating) {
this.daysRemaining = 0;
this.daysElapsed = 0;
return;
}
const now = new Date();
const end = new Date(this.config.endDate);
const start = new Date(this.config.startDate);
this.daysRemaining = Math.max(0, Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)));
this.daysElapsed = Math.max(0, Math.floor((now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)));
}
// 开始冬眠
async startHibernation() {
const today = new Date().toISOString().split('T')[0];
this.config = {
startDate: today,
endDate: this.editEndDate,
isHibernating: true,
targetTemp: this.editTargetTemp,
};
this.updateCountdown();
if (dataStore) {
await dataStore.put('hibernation_config', JSON.stringify(this.config));
await dataStore.flush();
}
}
// 结束冬眠
async endHibernation() {
this.config.isHibernating = false;
this.daysRemaining = 0;
this.daysElapsed = 0;
if (dataStore) {
await dataStore.put('hibernation_config', JSON.stringify(this.config));
await dataStore.flush();
}
}
// 获取进度百分比
getProgressPercent(): number {
if (!this.config.isHibernating) return 0;
const start = new Date(this.config.startDate).getTime();
const end = new Date(this.config.endDate).getTime();
const now = Date.now();
if (now <= start) return 0;
if (now >= end) return 100;
return Math.round(((now - start) / (end - start)) * 100);
}
build() {
Scroll() {
Column() {
Text('冬眠倒计时')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
if (this.config.isHibernating) {
// 冬眠中的显示
Column() {
Text('冬眠中')
.fontSize(18)
.fontColor('#2196F3')
.margin({ bottom: 16 })
// 倒计时大数字
Text(`${this.daysRemaining}`)
.fontSize(72)
.fontWeight(FontWeight.Bold)
.fontColor('#FF5722')
Text('天结束')
.fontSize(16)
.fontColor('#999999')
.margin({ bottom: 16 })
// 进度条
Progress({ value: this.getProgressPercent(), total: 100 })
.width('100%')
.color('#4CAF50')
.margin({ bottom: 8 })
Text(`已冬眠 ${this.daysElapsed} 天 | 进度 ${this.getProgressPercent()}%`)
.fontSize(13)
.fontColor('#999999')
.margin({ bottom: 16 })
// 冬眠信息
Row() {
Text(`开始:${this.config.startDate}`)
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
Text(`结束:${this.config.endDate}`)
.fontSize(13)
.fontColor('#666666')
.layoutWeight(1)
}
.width('100%')
.margin({ bottom: 8 })
Text(`目标温度:${this.config.targetTemp}度`)
.fontSize(13)
.fontColor('#666666')
.margin({ bottom: 16 })
Button('结束冬眠')
.width('100%')
.backgroundColor('#F44336')
.fontColor('#FFFFFF')
.onClick(() => { this.endHibernation(); })
}
.width('90%')
.padding(24)
.backgroundColor('#E3F2FD')
.borderRadius(16)
.margin({ bottom: 20 })
} else {
// 非冬眠期 - 设置界面
Column() {
Text('设置冬眠计划')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
Text('当前不在冬眠期')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 16 })
TextInput({ placeholder: '预计结束日期 YYYY-MM-DD', text: this.editEndDate })
.width('100%')
.onChange((value: string) => { this.editEndDate = value; })
.margin({ bottom: 12 })
Row() {
Text('目标温度')
.fontSize(14)
.width(80)
Text(`${this.editTargetTemp} 度`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.margin({ left: 8, right: 8 })
Slider({ min: 0, max: 15, step: 1, value: this.editTargetTemp, style: SliderStyle.OutSet })
.layoutWeight(1)
.onChange((value: number) => { this.editTargetTemp = value; })
}
.width('100%')
.margin({ bottom: 16 })
Button('开始冬眠')
.width('100%')
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
.onClick(() => { this.startHibernation(); })
}
.width('90%')
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.margin({ bottom: 20 })
}
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
}
}
这里有一个很重要的细节:aboutToDisappear。这个生命周期在组件即将销毁的时候调用,我们在这里清除定时器。如果不清除,定时器会一直运行,即使组件已经不存在了,这会造成内存泄漏。这跟React的useEffect返回清理函数是一个道理。
进度百分比的计算:用当前时间在开始和结束之间的位置来计算。如果还没开始,进度是0%;如果已经结束,进度是100%;否则按比例计算。
第三步:喂食频率日志
React版本
function FeedingLog() {
const [logs, setLogs] = useState(() => {
const saved = localStorage.getItem('feeding_logs');
return saved ? JSON.parse(saved) : [];
});
const addLog = (log) => {
const updated = [{ ...log, id: Date.now().toString(), date: new Date().toISOString().split('T')[0] }, ...logs];
setLogs(updated);
localStorage.setItem('feeding_logs', JSON.stringify(updated));
};
// 计算距上次喂食的天数
const getDaysSinceLastFeed = () => {
if (logs.length === 0) return '--';
const lastDate = new Date(logs[0].date);
return Math.floor((Date.now() - lastDate.getTime()) / (1000 * 60 * 60 * 24));
};
return (
<div className="feeding-log">
<h3>喂食日志</h3>
<p>距上次喂食:{getDaysSinceLastFeed()} 天</p>
{/* 表单和列表 */}
</div>
);
}
ArkTS版本:喂食频率日志
interface FeedingLog {
id: string;
date: string;
food: string; // 食物类型
amount: string; // 投喂量
response: string; // 龟的反应
note: string;
}
@Entry
@Component
struct FeedingLogPage {
@State logs: FeedingLog[] = [];
@State inputFood: string = '';
@State inputAmount: string = '';
@State inputResponse: number = 0;
@State inputNote: string = '';
private responseOptions: string[] = ['积极', '一般', '不进食'];
async aboutToAppear() {
if (!dataStore) return;
try {
const json = await dataStore.get('feeding_logs', '[]') as string;
this.logs = JSON.parse(json) as FeedingLog[];
} catch (err) {
console.error('加载喂食日志失败', JSON.stringify(err));
}
}
// 距上次喂食天数
getDaysSinceLastFeed(): string {
if (this.logs.length === 0) return '--';
const lastDate = new Date(this.logs[0].date);
const days = Math.floor((Date.now() - lastDate.getTime()) / (1000 * 60 * 60 * 24));
return `${days}`;
}
async addLog() {
const log: FeedingLog = {
id: Date.now().toString(),
date: new Date().toISOString().split('T')[0],
food: this.inputFood,
amount: this.inputAmount,
response: this.responseOptions[this.inputResponse],
note: this.inputNote,
};
this.logs = [log, ...this.logs];
if (dataStore) {
await dataStore.put('feeding_logs', JSON.stringify(this.logs));
await dataStore.flush();
}
this.inputFood = '';
this.inputAmount = '';
this.inputNote = '';
}
build() {
Scroll() {
Column() {
Text('喂食频率日志')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 上次喂食提示
Row() {
Column() {
Text(`${this.getDaysSinceLastFeed()}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor(this.logs.length === 0 ? '#999999' :
parseInt(this.getDaysSinceLastFeed()) > 7 ? '#F44336' : '#4CAF50')
Text('天前喂食')
.fontSize(13)
.fontColor('#999999')
}
.layoutWeight(1)
.padding(16)
.backgroundColor('#FFF3E0')
.borderRadius(12)
Column() {
Text(`${this.logs.length}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#2196F3')
Text('总喂食次数')
.fontSize(13)
.fontColor('#999999')
}
.layoutWeight(1)
.padding(16)
.backgroundColor('#E3F2FD')
.borderRadius(12)
}
.width('90%')
.space(12)
.margin({ bottom: 20 })
// 添加喂食记录
Column() {
Text('记录喂食')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
TextInput({ placeholder: '食物类型(如:龟粮、小鱼)', text: this.inputFood })
.width('100%')
.onChange((value: string) => { this.inputFood = value; })
.margin({ bottom: 8 })
TextInput({ placeholder: '投喂量(如:5粒、2条)', text: this.inputAmount })
.width('100%')
.onChange((value: string) => { this.inputAmount = value; })
.margin({ bottom: 8 })
Row() {
Text('反应')
.fontSize(14)
.width(60)
ForEach(this.responseOptions, (option: string, index: number) => {
Text(option)
.fontSize(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.inputResponse === index ? '#4CAF50' : '#E0E0E0')
.fontColor(this.inputResponse === index ? '#FFFFFF' : '#333333')
.borderRadius(16)
.onClick(() => { this.inputResponse = index; })
}, (_option: string, index: number) => `${index}`)
}
.width('100%')
.margin({ bottom: 8 })
TextArea({ placeholder: '备注...', text: this.inputNote })
.width('100%')
.height(50)
.onChange((value: string) => { this.inputNote = value; })
.margin({ bottom: 12 })
Button('记录喂食')
.width('100%')
.backgroundColor('#4CAF50')
.fontColor('#FFFFFF')
.onClick(() => { this.addLog(); })
}
.width('90%')
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.margin({ bottom: 20 })
// 喂食历史
if (this.logs.length > 0) {
Text('喂食历史')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('90%')
.margin({ bottom: 12 })
ForEach(this.logs, (log: FeedingLog) => {
Row() {
Text(log.date)
.fontSize(13)
.fontColor('#999999')
.width(90)
Text(log.food)
.fontSize(13)
.fontColor('#333333')
.width(60)
Text(log.amount)
.fontSize(13)
.fontColor('#666666')
.width(60)
Text(log.response)
.fontSize(13)
.fontColor(log.response === '积极' ? '#4CAF50' : log.response === '一般' ? '#FF9800' : '#F44336')
.layoutWeight(1)
}
.width('90%')
.padding(10)
.backgroundColor('#FAFAFA')
.borderRadius(6)
.margin({ bottom: 4 })
}, (log: FeedingLog) => log.id)
}
}
.width('100%')
.padding(20)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height('100%')
}
}
"距上次喂食"的天数用颜色来区分紧急程度:绿色表示最近喂过(7天以内),红色表示超过7天没喂了。这个阈值可以根据龟的品种和季节来调整,这里先用7天作为默认值。
流程图
flowchart TD
A[用户打开App] --> B[加载测量数据]
B --> C[显示当前背甲长度和体重]
C --> D[显示生长速率]
D --> E[用户添加新测量]
E --> F[保存到Preferences]
F --> G[更新统计数据]
A --> H[加载冬眠配置]
H --> I{是否在冬眠中?}
I -->|是| J[显示倒计时和进度]
I -->|否| K[显示冬眠设置界面]
J --> L[每分钟更新倒计时]
K --> M[用户设置结束日期和温度]
M --> N[开始冬眠]
N --> J
A --> O[加载喂食日志]
O --> P[显示距上次喂食天数]
P --> Q[用户记录喂食]
Q --> R[保存到Preferences]
R --> S[更新喂食间隔]
React vs ArkTS 对比表
| 功能点 | React (Web) | ArkTS (鸿蒙) |
|---|---|---|
| 定时器 | setInterval + useEffect清理 | setInterval + aboutToDisappear清理 |
| 条件渲染 | 三元表达式 | if/else语句 |
| 进度条 | div + CSS | Progress组件 |
| 天数计算 | Date差值 / 毫秒常量 | Date差值 / 毫秒常量(一致) |
| 颜色预警 | 条件className | 条件fontColor |
总结
这篇文章我们用「龟寿录」这个宠物龟记录App,学习了三个功能的实现:
-
背甲测量记录:用时序数组存储测量数据,用reduce计算生长速率,用卡片式布局展示当前数据和速率。
-
冬眠倒计时:用setInterval实现定时更新,用aboutToDisappear清理定时器,用Progress组件显示进度条,根据天数差值计算倒计时。
-
喂食频率日志:记录每次喂食的详细信息,计算距上次喂食的天数并用颜色预警,历史记录按时间倒序显示。
如果你对养龟感兴趣,欢迎去鸿蒙应用市场搜索「龟寿录」下载体验。