车辆保养App实战:Preferences存储与通知提醒的跨端实现(React Web + ArkTS鸿蒙)
如果你想做一个车辆保养App,需要用到Preferences和通知提醒
总是忘记车子什么时候该保养?打开鸿蒙应用市场,搜索"车管家",下载安装后就能管理车辆的保养记录了。自动计算下次保养时间和里程,还能设置提醒通知,再也不怕错过保养。
写在前面
大家好,今天聊一个跟之前都不太一样的App。前几篇主要讲数据存储,这篇要加入一个新元素:通知提醒。
"车管家"是一个车辆保养管理工具。它的核心功能是记录每次保养的信息(机油、轮胎、刹车片等),然后根据保养间隔(里程或时间)自动计算下次保养的时间,并在快到保养时间时发送通知提醒。
在Web端,通知用的是Notification API;在鸿蒙端,对应的是@ohos.notificationManager。两者的思路差不多:创建一个通知对象,设置标题和内容,然后发送出去。
这篇文章聊什么
- 保养记录的数据结构设计
- 里程和时间双重提醒的计算逻辑
- Web端Notification API的用法
- 鸿蒙端@ohos.notificationManager的用法
- Preferences存储 + 通知的组合使用
- 提醒判断的流程图
第一步:设计数据结构
// 保养类型
interface MaintenanceType {
name: string; // 名称:机油/轮胎/刹车片/空调滤芯/火花塞
intervalMileage: number; // 保养间隔里程(公里)
intervalMonths: number; // 保养间隔月数
category: string; // 分类:发动机/底盘/电气/车身
}
// 保养记录
interface MaintenanceRecord {
id: string;
typeName: string; // 保养类型名称
mileage: number; // 保养时的里程数
cost: number; // 花费
date: string; // 保养日期
location: string; // 保养地点
note: string; // 备注
nextMileage: number; // 下次保养里程
nextDate: string; // 下次保养日期
}
// 车辆信息
interface Vehicle {
id: string;
name: string; // 车辆昵称
brand: string; // 品牌
model: string; // 车型
currentMileage: number; // 当前里程
purchaseDate: string; // 购车日期
records: MaintenanceRecord[]; // 保养记录
}
这里的关键是nextMileage和nextDate。每次保养后,根据保养类型的间隔自动计算下次保养的里程和时间。比如机油每5000公里或6个月保养一次,那么上次在10000公里做的保养,下次就是15000公里或6个月后。
第二步:React版本 – localStorage + Notification
2.1 保养记录管理
import React, { useState, useEffect } from 'react';
// 预设保养类型
const MAINTENANCE_TYPES = [
{ name: '机油', intervalMileage: 5000, intervalMonths: 6, category: '发动机' },
{ name: '机油滤芯', intervalMileage: 5000, intervalMonths: 6, category: '发动机' },
{ name: '空气滤芯', intervalMileage: 10000, intervalMonths: 12, category: '发动机' },
{ name: '刹车片', intervalMileage: 30000, intervalMonths: 24, category: '底盘' },
{ name: '轮胎', intervalMileage: 40000, intervalMonths: 36, category: '底盘' },
{ name: '空调滤芯', intervalMileage: 10000, intervalMonths: 12, category: '车身' },
{ name: '火花塞', intervalMileage: 20000, intervalMonths: 24, category: '电气' }
];
function CarManager() {
const [vehicle, setVehicle] = useState(null);
const [records, setRecords] = useState([]);
useEffect(() => {
const savedVehicle = localStorage.getItem('cheguanjia_vehicle');
const savedRecords = localStorage.getItem('cheguanjia_records');
if (savedVehicle) setVehicle(JSON.parse(savedVehicle));
if (savedRecords) setRecords(JSON.parse(savedRecords));
}, []);
useEffect(() => {
if (records.length > 0) {
localStorage.setItem('cheguanjia_records', JSON.stringify(records));
}
}, [records]);
// 添加保养记录
const addRecord = (typeName, mileage, cost, location, note) => {
const type = MAINTENANCE_TYPES.find(t => t.name === typeName);
if (!type) return;
const record = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
typeName,
mileage,
cost,
date: new Date().toISOString().split('T')[0],
location,
note,
nextMileage: mileage + type.intervalMileage,
nextDate: getNextDate(type.intervalMonths)
};
setRecords(prev => [...prev, record].sort((a, b) => b.mileage - a.mileage));
};
// 计算下次保养日期
function getNextDate(months) {
const date = new Date();
date.setMonth(date.getMonth() + months);
return date.toISOString().split('T')[0];
}
// 检查是否需要提醒
const getUpcomingMaintenance = () => {
const today = new Date().toISOString().split('T')[0];
const currentMileage = vehicle?.currentMileage || 0;
const reminderDays = 30; // 提前30天提醒
const reminderMileage = 500; // 提前500公里提醒
return records.filter(record => {
const daysUntilNext = Math.ceil(
(new Date(record.nextDate).getTime() - new Date(today).getTime()) / (1000 * 60 * 60 * 24)
);
const mileageUntilNext = record.nextMileage - currentMileage;
return daysUntilNext <= reminderDays || mileageUntilNext <= reminderMileage;
});
};
// 发送浏览器通知
const sendNotification = (title, body) => {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body });
} else if ('Notification' in window && Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification(title, { body });
}
});
}
};
// 统计总花费
const getTotalCost = () => {
return records.reduce((sum, r) => sum + r.cost, 0);
};
return (
<div className="car-manager">
<h1>车管家</h1>
{/* 提醒区域 */}
{getUpcomingMaintenance().length > 0 && (
<div className="reminder-section">
<h3>保养提醒</h3>
{getUpcomingMaintenance().map(record => (
<div key={record.id} className="reminder-item">
<p>{record.typeName}:下次保养 {record.nextMileage}km 或 {record.nextDate}</p>
<button onClick={() => sendNotification(
'保养提醒',
`${record.typeName}即将到保养时间,请尽快安排`
)}>发送提醒</button>
</div>
))}
</div>
)}
{/* 统计信息 */}
<div className="stats">
<p>总保养次数:{records.length}</p>
<p>总花费:{getTotalCost()}元</p>
</div>
{/* 保养记录列表 */}
<div className="record-list">
{records.map(record => (
<div key={record.id} className="record-card">
<h4>{record.typeName}</h4>
<p>里程:{record.mileage}km | 花费:{record.cost}元</p>
<p>下次保养:{record.nextMileage}km / {record.nextDate}</p>
</div>
))}
</div>
</div>
);
}
2.2 Web端Notification要点
Web端的Notification API需要用户授权。第一次调用时需要requestPermission,用户同意后才能发送通知。这个授权是一次性的,用户同意后后续不需要再请求。
// 检查通知权限
if ('Notification' in window) {
if (Notification.permission === 'granted') {
// 已授权,直接发送
new Notification('标题', { body: '内容' });
} else if (Notification.permission !== 'denied') {
// 未授权也未拒绝,请求授权
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification('标题', { body: '内容' });
}
});
}
}
第三步:ArkTS版本 – Preferences + notificationManager
3.1 导入依赖
import { preferences } from '@kit.ArkData';
import { notificationManager } from '@kit.ArkNotification';
import { common } from '@kit.AbilityKit';
import { wantAgent } from '@kit.AbilityKit';
这里多导入了两个模块:notificationManager用于发送通知,wantAgent用于点击通知后跳转到指定页面。
3.2 通知发送方法
// 发送保养提醒通知
async function sendMaintenanceNotification(title: string, content: string) {
try {
// 创建通知请求
let request: notificationManager.NotificationRequest = {
id: Date.now(),
content: {
contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: title,
text: content
}
}
};
// 发送通知
await notificationManager.publish(request);
console.info('通知发送成功');
} catch (err) {
console.error('通知发送失败', JSON.stringify(err));
}
}
这里有几个要点:
NotificationRequest:通知请求对象,包含id和content。id是通知的唯一标识,content是通知的内容。content有多种类型,我们用的是BASIC_TEXT,就是最简单的文字通知。
contentType:通知内容类型,BASIC_TEXT是最常用的。还有LONG_TEXT(长文本)、PICTURE(图片)等类型。
publish:发送通知的方法,是异步的。发送成功后,通知会显示在系统的通知栏里。
3.3 完整页面组件
interface MaintenanceRecord {
id: string;
typeName: string;
mileage: number;
cost: number;
date: string;
location: string;
note: string;
nextMileage: number;
nextDate: string;
}
interface VehicleInfo {
name: string;
brand: string;
model: string;
currentMileage: number;
purchaseDate: string;
}
const MAINTENANCE_TYPES = [
{ name: '机油', intervalMileage: 5000, intervalMonths: 6, category: '发动机' },
{ name: '机油滤芯', intervalMileage: 5000, intervalMonths: 6, category: '发动机' },
{ name: '空气滤芯', intervalMileage: 10000, intervalMonths: 12, category: '发动机' },
{ name: '刹车片', intervalMileage: 30000, intervalMonths: 24, category: '底盘' },
{ name: '轮胎', intervalMileage: 40000, intervalMonths: 36, category: '底盘' },
{ name: '空调滤芯', intervalMileage: 10000, intervalMonths: 12, category: '车身' },
{ name: '火花塞', intervalMileage: 20000, intervalMonths: 24, category: '电气' }
];
@Entry
@Component
struct CarManagerPage {
@State vehicle: VehicleInfo = { name: '', brand: '', model: '', currentMileage: 0, purchaseDate: '' };
@State records: MaintenanceRecord[] = [];
@State upcomingList: MaintenanceRecord[] = [];
private preferencesStore: preferences.Preferences | null = null;
async aboutToAppear() {
let context = getContext(this) as common.UIAbilityContext;
this.preferencesStore = await preferences.getPreferences(context, 'cheguanjia_store');
// 加载车辆信息
const vehicleData = await this.preferencesStore.get('vehicle', '');
if (vehicleData && typeof vehicleData === 'string' && vehicleData.length > 0) {
this.vehicle = JSON.parse(vehicleData) as VehicleInfo;
}
// 加载保养记录
const recordsData = await this.preferencesStore.get('records', '');
if (recordsData && typeof recordsData === 'string' && recordsData.length > 0) {
this.records = JSON.parse(recordsData) as MaintenanceRecord[];
}
// 检查提醒
this.checkReminders();
}
// 保存数据
async saveData() {
if (!this.preferencesStore) return;
await this.preferencesStore.put('vehicle', JSON.stringify(this.vehicle));
await this.preferencesStore.put('records', JSON.stringify(this.records));
await this.preferencesStore.flush();
}
// 计算下次保养日期
getNextDate(months: number): string {
const date = new Date();
date.setMonth(date.getMonth() + months);
return date.toISOString().split('T')[0];
}
// 添加保养记录
async addRecord(typeName: string, mileage: number, cost: number, location: string, note: string) {
const type = MAINTENANCE_TYPES.find(t => t.name === typeName);
if (!type) return;
const record: MaintenanceRecord = {
id: Date.now().toString(36) + Math.random().toString(36).substring(2),
typeName,
mileage,
cost,
date: new Date().toISOString().split('T')[0],
location,
note,
nextMileage: mileage + type.intervalMileage,
nextDate: this.getNextDate(type.intervalMonths)
};
this.records = [record, ...this.records].sort((a, b) => b.mileage - a.mileage);
await this.saveData();
this.checkReminders();
}
// 检查保养提醒
checkReminders() {
const today = new Date().toISOString().split('T')[0];
const reminderDays = 30;
const reminderMileage = 500;
this.upcomingList = this.records.filter(record => {
const daysUntilNext = Math.ceil(
(new Date(record.nextDate).getTime() - new Date(today).getTime()) / (1000 * 60 * 60 * 24)
);
const mileageUntilNext = record.nextMileage - this.vehicle.currentMileage;
return daysUntilNext <= reminderDays || mileageUntilNext <= reminderMileage;
});
}
// 发送通知
async sendNotification(title: string, content: string) {
try {
let request: notificationManager.NotificationRequest = {
id: Date.now(),
content: {
contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: title,
text: content
}
}
};
await notificationManager.publish(request);
} catch (err) {
console.error('通知发送失败', JSON.stringify(err));
}
}
// 统计总花费
getTotalCost(): number {
return this.records.reduce((sum, r) => sum + r.cost, 0);
}
build() {
Column() {
Text('车管家')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 16 })
// 车辆信息卡片
Column() {
Text(this.vehicle.name || '未设置车辆')
.fontSize(18)
.fontWeight(FontWeight.Bold)
if (this.vehicle.brand) {
Text(`${this.vehicle.brand} ${this.vehicle.model}`)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 4 })
}
Text(`当前里程:${this.vehicle.currentMileage}km`)
.fontSize(14)
.fontColor('#333333')
.margin({ top: 4 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 16 })
// 保养提醒区域
if (this.upcomingList.length > 0) {
Column() {
Text('保养提醒')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF4444')
.margin({ bottom: 8 })
ForEach(this.upcomingList, (record: MaintenanceRecord) => {
Row() {
Column() {
Text(record.typeName)
.fontSize(14)
.fontWeight(FontWeight.Bold)
Text(`下次:${record.nextMileage}km / ${record.nextDate}`)
.fontSize(12)
.fontColor('#FF6B6B')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Button('提醒')
.fontSize(12)
.backgroundColor('#FF4444')
.fontColor('#FFFFFF')
.onClick(() => {
this.sendNotification(
'保养提醒',
`${record.typeName}即将到保养时间,请尽快安排`
);
})
}
.width('100%')
.padding(12)
.backgroundColor('#FFF5F5')
.borderRadius(8)
.margin({ bottom: 6 })
}, (record: MaintenanceRecord) => record.id)
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 16 })
}
// 统计信息
Row() {
Text(`保养次数:${this.records.length}`)
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
Text(`总花费:${this.getTotalCost()}元`)
.fontSize(14)
.fontColor('#FF6B6B')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 16 })
// 保养记录列表
List({ space: 8 }) {
ForEach(this.records, (record: MaintenanceRecord) => {
ListItem() {
Column() {
Row() {
Text(record.typeName)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Text(record.date)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.margin({ bottom: 6 })
Row() {
Text(`${record.mileage}km`)
.fontSize(13)
.fontColor('#333333')
.margin({ right: 12 })
Text(`${record.cost}元`)
.fontSize(13)
.fontColor('#FF6B6B')
.margin({ right: 12 })
Text(record.location || '未记录')
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.margin({ bottom: 6 })
Text(`下次保养:${record.nextMileage}km / ${record.nextDate}`)
.fontSize(12)
.fontColor('#4ECDC4')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 4, color: '#00000010', offsetY: 2 })
}
}, (record: MaintenanceRecord) => record.id)
}
.width('100%')
.padding({ left: 16, right: 16 })
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
}
第四步:提醒判断流程图
React vs ArkTS 对比表
| 功能 | React (Web) | ArkTS (鸿蒙) |
|---|---|---|
| 通知权限 | Notification.requestPermission | module.json5声明 + 动态授权 |
| 发送通知 | new Notification(title, options) | notificationManager.publish(request) |
| 通知内容 | title + body | contentType + normal对象 |
| 通知ID | 自动生成 | 手动指定 |
| 里程计算 | 纯JS逻辑 | 纯JS逻辑(一样) |
| 日期计算 | Date对象 + setMonth | Date对象 + setMonth(一样) |
鸿蒙发送通知需要在module.json5中声明通知权限,这一点跟Web端不同。Web端是在运行时请求权限,鸿蒙端是先在配置文件中声明,然后在运行时也可以动态请求。
总结
这篇文章我们用"车管家"这个车辆保养管理App,演示了Preferences存储和通知提醒的组合使用。核心要点:
- 里程和时间双重提醒的计算逻辑:分别计算剩余天数和剩余里程
- Web端Notification API需要用户授权,鸿蒙端notificationManager需要在配置文件中声明权限
- 通知的创建和发送都是异步操作
- Preferences存储车辆信息和保养记录,提醒数据通过计算得出,不需要额外存储
通知功能让App从"被动记录"变成了"主动提醒",用户体验提升很大。下一篇我们会聊"改车志",演示多维筛选和排序逻辑。
更多推荐



所有评论(0)