
鸿蒙经期预测助手应用开发指南 原创
鸿蒙经期预测助手应用开发指南
一、系统架构设计
基于HarmonyOS的分布式能力和AI技术,
周期记录:记录用户月经周期历史数据
智能预测:AI算法预测未来月经周期和排卵期
健康建议:根据周期阶段提供个性化健康建议
多设备同步:跨设备同步经期数据和提醒
异常预警:识别异常周期并提供医疗建议
!https://example.com/harmony-period-tracker-arch.png
二、核心代码实现
周期记录服务
// PeriodRecordService.ets
import distributedData from ‘@ohos.data.distributedData’;
class PeriodRecordService {
private static instance: PeriodRecordService;
private kvManager: distributedData.KVManager;
private kvStore: distributedData.KVStore;
private constructor() {
this.initKVStore();
private async initKVStore(): Promise<void> {
const config = {
bundleName: 'com.example.periodTracker',
userInfo: { userId: 'currentUser' }
};
this.kvManager = distributedData.createKVManager(config);
this.kvStore = await this.kvManager.getKVStore('period_records', {
createIfMissing: true
});
this.kvStore.on('dataChange', (data) => {
this.handleRemoteUpdate(data);
});
public static getInstance(): PeriodRecordService {
if (!PeriodRecordService.instance) {
PeriodRecordService.instance = new PeriodRecordService();
return PeriodRecordService.instance;
public async recordPeriodStart(date: string): Promise<void> {
const records = await this.getRecords();
records.push({
id: generateId(),
startDate: date,
endDate: null,
symptoms: [],
flowLevel: 'medium',
createdAt: Date.now()
});
await this.kvStore.put('records', JSON.stringify(records));
predictionService.updatePredictions();
public async recordPeriodEnd(date: string): Promise<void> {
const records = await this.getRecords();
const lastRecord = records[records.length - 1];
if (lastRecord && !lastRecord.endDate) {
lastRecord.endDate = date;
await this.kvStore.put('records', JSON.stringify(records));
predictionService.updatePredictions();
}
public async addSymptom(recordId: string, symptom: Symptom): Promise<void> {
const records = await this.getRecords();
const record = records.find(r => r.id === recordId);
if (record) {
record.symptoms.push(symptom);
await this.kvStore.put('records', JSON.stringify(records));
}
public async getRecords(): Promise<PeriodRecord[]> {
const value = await this.kvStore.get(‘records’);
return value ? JSON.parse(value) : [];
public async getLastRecord(): Promise<PeriodRecord | null> {
const records = await this.getRecords();
return records.length > 0 ? records[records.length - 1] : null;
private handleRemoteUpdate(data: distributedData.ChangeInfo): void {
if (data.deviceId === deviceInfo.deviceId) return;
if (data.key === 'records') {
const records = JSON.parse(data.value);
EventBus.emit('periodRecordsUpdated', records);
}
export const periodRecordService = PeriodRecordService.getInstance();
周期预测服务
// PredictionService.ets
class PredictionService {
private static instance: PredictionService;
private constructor() {}
public static getInstance(): PredictionService {
if (!PredictionService.instance) {
PredictionService.instance = new PredictionService();
return PredictionService.instance;
public async predictNextPeriod(records: PeriodRecord[]): Promise<PeriodPrediction> {
if (records.length < 3) {
return this.getDefaultPrediction();
const cycles = this.calculateCycleLengths(records);
const avgCycle = this.calculateAverageCycle(cycles);
const lastPeriod = records[records.length - 1];
return {
nextPeriodStart: this.addDays(lastPeriod.startDate, avgCycle),
ovulationDate: this.addDays(lastPeriod.startDate, Math.floor(avgCycle / 2)),
fertileWindow: {
start: this.addDays(lastPeriod.startDate, Math.floor(avgCycle / 2) - 3),
end: this.addDays(lastPeriod.startDate, Math.floor(avgCycle / 2) + 1)
},
confidence: this.calculateConfidence(cycles),
createdAt: Date.now()
};
public async predictSymptoms(nextPeriodStart: string): Promise<SymptomPrediction[]> {
const records = await periodRecordService.getRecords();
const similarPeriods = this.findSimilarPeriods(records, nextPeriodStart);
if (similarPeriods.length === 0) {
return this.getDefaultSymptoms();
const symptomFrequency: Record<string, number> = {};
similarPeriods.forEach(record => {
record.symptoms.forEach(symptom => {
symptomFrequency[symptom.type] = (symptomFrequency[symptom.type] || 0) + 1;
});
});
return Object.entries(symptomFrequency)
.map(([type, count]) => ({
type,
probability: count / similarPeriods.length,
severity: this.calculateAverageSeverity(similarPeriods, type)
}))
.sort((a, b) => b.probability - a.probability);
private calculateCycleLengths(records: PeriodRecord[]): number[] {
const lengths: number[] = [];
for (let i = 1; i < records.length; i++) {
const prev = new Date(records[i - 1].startDate);
const current = new Date(records[i].startDate);
lengths.push((current.getTime() - prev.getTime()) / (24 60 60 * 1000));
return lengths;
private calculateAverageCycle(cycles: number[]): number {
const validCycles = cycles.filter(days => days >= 21 && days <= 35);
if (validCycles.length === 0) return 28; // 默认28天
return Math.round(validCycles.reduce((sum, days) => sum + days, 0) / validCycles.length);
private calculateConfidence(cycles: number[]): number {
if (cycles.length < 3) return 0.5;
const avg = this.calculateAverageCycle(cycles);
const variance = cycles.reduce((sum, days) => sum + Math.pow(days - avg, 2), 0) / cycles.length;
const stdDev = Math.sqrt(variance);
// 标准差越小,置信度越高
return Math.max(0.1, 1 - stdDev / 7);
private findSimilarPeriods(records: PeriodRecord[], targetDate: string): PeriodRecord[] {
const targetMonth = new Date(targetDate).getMonth();
return records.filter(record => {
const recordMonth = new Date(record.startDate).getMonth();
return recordMonth === targetMonth;
});
private calculateAverageSeverity(records: PeriodRecord[], symptomType: string): number {
const symptoms = records
.flatMap(record => record.symptoms)
.filter(symptom => symptom.type === symptomType);
if (symptoms.length === 0) return 0;
return symptoms.reduce((sum, symptom) => sum + symptom.severity, 0) / symptoms.length;
private addDays(dateStr: string, days: number): string {
const date = new Date(dateStr);
date.setDate(date.getDate() + days);
return date.toISOString().split('T')[0];
private getDefaultPrediction(): PeriodPrediction {
const now = new Date();
return {
nextPeriodStart: this.addDays(now.toISOString().split('T')[0], 28),
ovulationDate: this.addDays(now.toISOString().split('T')[0], 14),
fertileWindow: {
start: this.addDays(now.toISOString().split('T')[0], 11),
end: this.addDays(now.toISOString().split('T')[0], 15)
},
confidence: 0.5,
createdAt: Date.now()
};
private getDefaultSymptoms(): SymptomPrediction[] {
return [
type: ‘cramps’, probability: 0.7, severity: 2 },
type: ‘headache’, probability: 0.5, severity: 1 },
type: ‘bloating’, probability: 0.6, severity: 1 }
];
}
export const predictionService = PredictionService.getInstance();
健康建议服务
// HealthAdviceService.ets
class HealthAdviceService {
private static instance: HealthAdviceService;
private constructor() {}
public static getInstance(): HealthAdviceService {
if (!HealthAdviceService.instance) {
HealthAdviceService.instance = new HealthAdviceService();
return HealthAdviceService.instance;
public getCyclePhase(date: string, prediction: PeriodPrediction): CyclePhase {
const currentDate = new Date(date);
const periodStart = new Date(prediction.nextPeriodStart);
const ovulationDate = new Date(prediction.ovulationDate);
const daysBeforePeriod = (periodStart.getTime() - currentDate.getTime()) / (24 60 60 * 1000);
if (daysBeforePeriod <= 0 && daysBeforePeriod > -7) {
return 'menstrual';
else if (currentDate >= new Date(prediction.fertileWindow.start) &&
currentDate <= new Date(prediction.fertileWindow.end)) {
return 'fertile';
else if (currentDate > new Date(prediction.ovulationDate) &&
daysBeforePeriod > 7) {
return 'luteal';
else {
return 'follicular';
}
public getAdviceForPhase(phase: CyclePhase): HealthAdvice[] {
const adviceMap: Record<CyclePhase, HealthAdvice[]> = {
menstrual: [
category: ‘nutrition’, content: ‘增加富含铁的食物摄入,如红肉、菠菜等’ },
category: ‘exercise’, content: ‘适度运动,如瑜伽或散步,帮助缓解痉挛’ },
category: ‘rest’, content: ‘保证充足睡眠,每天7-8小时’ }
],
follicular: [
category: ‘exercise’, content: ‘这是进行高强度训练的最佳时期’ },
category: ‘nutrition’, content: ‘增加蛋白质摄入支持肌肉恢复’ },
category: ‘general’, content: ‘适合开始新项目或制定计划’ }
],
fertile: [
category: ‘hydration’, content: ‘多喝水保持身体水分充足’ },
category: ‘exercise’, content: ‘适度运动,避免过度疲劳’ },
category: ‘general’, content: ‘社交活动的最佳时期’ }
],
luteal: [
category: ‘nutrition’, content: ‘增加复合碳水化合物摄入稳定情绪’ },
category: ‘exercise’, content: ‘适度运动如游泳或骑行’ },
category: ‘rest’, content: ‘可能需要更多休息时间’ }
};
return adviceMap[phase] || [];
public getSymptomReliefAdvice(symptom: string): SymptomAdvice {
const adviceMap: Record<string, SymptomAdvice> = {
cramps: {
symptom: 'cramps',
remedies: [
'热敷下腹部',
'轻度按摩',
'服用姜茶或薄荷茶',
'适度运动如散步'
],
warning: '如果疼痛持续超过3天或非常剧烈,请咨询医生'
},
headache: {
symptom: 'headache',
remedies: [
'保持充足水分',
'在安静黑暗的房间休息',
'适度按摩太阳穴',
'服用医生推荐的止痛药'
],
warning: '如果伴随视力模糊或呕吐,请立即就医'
},
bloating: {
symptom: 'bloating',
remedies: [
'减少盐分摄入',
'多喝水',
'避免碳酸饮料',
'进行轻度运动促进消化'
],
warning: '如果伴随严重腹痛或体重骤增,请咨询医生'
};
return adviceMap[symptom] || {
symptom,
remedies: ['保持健康饮食和适度运动'],
warning: '如果症状持续或加重,请咨询医生'
};
public checkForAbnormalities(records: PeriodRecord[]): HealthAlert[] {
const alerts: HealthAlert[] = [];
const cycles = this.calculateCycleLengths(records);
// 检查周期长度异常
if (cycles.some(days => days < 21 || days > 35)) {
alerts.push({
type: 'irregular_cycle',
message: '检测到不规则月经周期',
severity: 'moderate',
suggestion: '建议记录至少3个月周期后咨询妇科医生'
});
// 检查经期长度异常
if (records.some(record => {
if (!record.endDate) return false;
const start = new Date(record.startDate);
const end = new Date(record.endDate);
const days = (end.getTime() - start.getTime()) / (24 60 60 * 1000) + 1;
return days < 3 || days > 7;
})) {
alerts.push({
type: 'abnormal_duration',
message: '检测到异常经期长度',
severity: 'moderate',
suggestion: '经期短于3天或长于7天建议咨询医生'
});
// 检查严重症状
const severeSymptoms = records.flatMap(record =>
record.symptoms.filter(s => s.severity >= 4)
);
if (severeSymptoms.length > 2) {
alerts.push({
type: 'severe_symptoms',
message: '检测到多次严重经期症状',
severity: 'high',
suggestion: '建议尽快预约妇科医生检查'
});
return alerts;
private calculateCycleLengths(records: PeriodRecord[]): number[] {
const lengths: number[] = [];
for (let i = 1; i < records.length; i++) {
const prev = new Date(records[i - 1].startDate);
const current = new Date(records[i].startDate);
lengths.push((current.getTime() - prev.getTime()) / (24 60 60 * 1000));
return lengths;
}
export const healthAdviceService = HealthAdviceService.getInstance();
三、主界面实现
日历视图
// CalendarView.ets
@Component
struct CalendarView {
@State records: PeriodRecord[] = [];
@State prediction: PeriodPrediction | null = null;
@State selectedDate: string = new Date().toISOString().split(‘T’)[0];
aboutToAppear() {
this.loadData();
EventBus.on(‘periodRecordsUpdated’, () => this.loadData());
build() {
Column() {
// 月份导航
Row() {
Button('<')
.onClick(() => this.changeMonth(-1))
Text(this.getMonthYear())
.fontSize(18)
.margin({ left: 16, right: 16 })
Button('>')
.onClick(() => this.changeMonth(1))
.margin({ top: 16 })
// 日历网格
Grid() {
// 星期标题
ForEach(['日', '一', '二', '三', '四', '五', '六'], (day) => {
GridItem() {
Text(day)
.fontSize(16)
})
// 日历日期
ForEach(this.getCalendarDays(), (day) => {
GridItem() {
CalendarDayCell({
day,
records: this.records,
prediction: this.prediction,
isSelected: day.date === this.selectedDate
})
})
.columnsTemplate(‘1fr 1fr 1fr 1fr 1fr 1fr 1fr’)
.rowsTemplate('40px ' + '1fr '.repeat(6))
.height('60%')
.margin({ top: 16 })
// 选中日期的详细信息
if (this.selectedDate) {
DayDetailView({
date: this.selectedDate,
records: this.records,
prediction: this.prediction
})
.margin({ top: 16 })
}
.padding(16)
private async loadData(): Promise<void> {
this.records = await periodRecordService.getRecords();
this.prediction = await predictionService.predictNextPeriod(this.records);
private getCalendarDays(): CalendarDay[] {
const [year, month] = this.selectedDate.split('-').map(Number);
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const startDay = firstDay.getDay();
const daysInMonth = lastDay.getDate();
const days: CalendarDay[] = [];
// 上个月的最后几天
const prevMonthLastDay = new Date(year, month - 1, 0).getDate();
for (let i = startDay - 1; i >= 0; i--) {
days.push({
day: prevMonthLastDay - i,
date: {year}-{month - 1}-${prevMonthLastDay - i},
isCurrentMonth: false
});
// 当月日期
for (let i = 1; i <= daysInMonth; i++) {
days.push({
day: i,
date: {year}-{month}-${i},
isCurrentMonth: true
});
// 下个月的前几天
const remainingCells = 42 - days.length; // 6行x7列
for (let i = 1; i <= remainingCells; i++) {
days.push({
day: i,
date: {year}-{month + 1}-${i},
isCurrentMonth: false
});
return days;
private getMonthYear(): string {
const [year, month] = this.selectedDate.split('-');
return {year}年{parseInt(month)}月;
private changeMonth(offset: number): void {
const [year, month] = this.selectedDate.split('-').map(Number);
const newDate = new Date(year, month - 1 + offset, 1);
this.selectedDate = {newDate.getFullYear()}-{newDate.getMonth() + 1}-1;
}
@Component
struct CalendarDayCell {
private day: CalendarDay;
private records: PeriodRecord[];
private prediction: PeriodPrediction | null;
private isSelected: boolean;
build() {
Column() {
Text(this.day.day.toString())
.fontSize(16)
.fontColor(this.day.isCurrentMonth ?
(this.isSelected ? ‘#FFFFFF’ : ‘#000000’) : ‘#999999’)
// 标记月经期
if (this.isPeriodDay()) {
Circle()
.width(6)
.height(6)
.backgroundColor('#FF4081')
.margin({ top: 4 })
// 标记预测经期
if (this.isPredictedPeriodDay()) {
Circle()
.width(6)
.height(6)
.backgroundColor('#FF408150')
.margin({ top: 4 })
// 标记排卵期
if (this.isOvulationDay()) {
Circle()
.width(6)
.height(6)
.backgroundColor('#4CAF50')
.margin({ top: 4 })
}
.height(60)
.backgroundColor(this.isSelected ? '#2196F3' : 'transparent')
.borderRadius(4)
.onClick(() => {
if (this.day.isCurrentMonth) {
EventBus.emit('daySelected', this.day.date);
})
private isPeriodDay(): boolean {
return this.records.some(record =>
this.isDateBetween(this.day.date, record.startDate, record.endDate || record.startDate)
);
private isPredictedPeriodDay(): boolean {
if (!this.prediction) return false;
const predictedStart = this.prediction.nextPeriodStart;
const predictedEnd = this.addDays(predictedStart, 5); // 假设经期5天
return this.isDateBetween(this.day.date, predictedStart, predictedEnd);
private isOvulationDay(): boolean {
if (!this.prediction) return false;
const ovulationDate = this.prediction.ovulationDate;
return this.day.date === ovulationDate;
private isDateBetween(date: string, start: string, end: string): boolean {
const current = new Date(date);
const startDate = new Date(start);
const endDate = new Date(end);
return current >= startDate && current <= endDate;
private addDays(dateStr: string, days: number): string {
const date = new Date(dateStr);
date.setDate(date.getDate() + days);
return date.toISOString().split('T')[0];
}
@Component
struct DayDetailView {
private date: string;
private records: PeriodRecord[];
private prediction: PeriodPrediction | null;
build() {
Column() {
Text(this.getDateText())
.fontSize(18)
.fontWeight(FontWeight.Bold)
if (this.isPeriodDay()) {
Text('月经期')
.fontSize(16)
.fontColor('#FF4081')
.margin({ top: 8 })
else if (this.isPredictedPeriodDay()) {
Text('预测月经期')
.fontSize(16)
.fontColor('#FF4081')
.margin({ top: 8 })
else if (this.isOvulationDay()) {
Text('排卵日')
.fontSize(16)
.fontColor('#4CAF50')
.margin({ top: 8 })
else if (this.isFertileDay()) {
Text('易孕期')
.fontSize(16)
.fontColor('#4CAF50')
.margin({ top: 8 })
// 显示周期阶段和建议
if (this.prediction) {
const phase = healthAdviceService.getCyclePhase(this.date, this.prediction);
Text(周期阶段: ${this.getPhaseName(phase)})
.fontSize(16)
.margin({ top: 8 })
// 健康建议
Text('健康建议:')
.fontSize(16)
.margin({ top: 8 })
ForEach(healthAdviceService.getAdviceForPhase(phase), (advice) => {
Text(• ${advice.content})
.fontSize(14)
.margin({ top: 4 })
})
// 记录操作按钮
if (this.isPeriodDay()) {
Button('记录经期结束')
.onClick(() => periodRecordService.recordPeriodEnd(this.date))
.margin({ top: 16 })
else {
Button('记录经期开始')
.onClick(() => periodRecordService.recordPeriodStart(this.date))
.margin({ top: 16 })
}
.padding(16)
.backgroundColor('#F5F5F5')
.borderRadius(8)
private getDateText(): string {
const date = new Date(this.date);
return {date.getFullYear()}年{date.getMonth() + 1}月${date.getDate()}日;
private isPeriodDay(): boolean {
return this.records.some(record =>
this.isDateBetween(this.date, record.startDate, record.endDate || record.startDate)
);
private isPredictedPeriodDay(): boolean {
if (!this.prediction) return false;
const predictedStart = this.prediction.nextPeriodStart;
const predictedEnd = this.addDays(predictedStart, 5); // 假设经期5天
return this.isDateBetween(this.date, predictedStart, predictedEnd);
private isOvulationDay(): boolean {
if (!this.prediction) return false;
return this.date === this.prediction.ovulationDate;
private isFertileDay(): boolean {
if (!this.prediction) return false;
return this.isDateBetween(
this.date,
this.prediction.fertileWindow.start,
this.prediction.fertileWindow.end
);
private getPhaseName(phase: CyclePhase): string {
const names: Record<CyclePhase, string> = {
menstrual: '月经期',
follicular: '卵泡期',
ovulation: '排卵期',
luteal: '黄体期'
};
return names[phase] || phase;
}
预测视图
// PredictionView.ets
@Component
struct PredictionView {
@State prediction: PeriodPrediction | null = null;
@State symptomPredictions: SymptomPrediction[] = [];
@State alerts: HealthAlert[] = [];
aboutToAppear() {
this.loadData();
EventBus.on(‘periodRecordsUpdated’, () => this.loadData());
build() {
Column() {
Text('周期预测')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 16 })
if (!this.prediction) {
LoadingProgress()
.width(50)
.height(50)
.margin({ top: 32 })
else {
// 下次经期预测
Column() {
Text('下次经期')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text(formatDate(this.prediction.nextPeriodStart))
.fontSize(24)
.fontColor('#FF4081')
.margin({ top: 4 })
Text(置信度: ${Math.round(this.prediction.confidence * 100)}%)
.fontSize(14)
.fontColor('#666666')
.padding(16)
.backgroundColor('#FFF5F7')
.borderRadius(8)
.width('100%')
// 排卵期预测
Column() {
Text('排卵日')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text(formatDate(this.prediction.ovulationDate))
.fontSize(24)
.fontColor('#4CAF50')
.margin({ top: 4 })
Text(易孕期: {formatDate(this.prediction.fertileWindow.start)} 至 {formatDate(this.prediction.fertileWindow.end)})
.fontSize(14)
.fontColor('#666666')
.padding(16)
.backgroundColor('#F1F8E9')
.borderRadius(8)
.width('100%')
.margin({ top: 16 })
// 症状预测
if (this.symptomPredictions.length > 0) {
Column() {
Text('可能出现的症状')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
ForEach(this.symptomPredictions, (prediction) => {
Row() {
Text(this.getSymptomName(prediction.type))
.fontSize(16)
.layoutWeight(1)
Text(${Math.round(prediction.probability * 100)}%)
.fontSize(16)
.fontColor(this.getSeverityColor(prediction.severity))
.padding(8)
})
.padding(16)
.backgroundColor('#FFF3E0')
.borderRadius(8)
.width('100%')
.margin({ top: 16 })
// 健康预警
if (this.alerts.length > 0) {
Column() {
Text('健康预警')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#F44336')
.margin({ bottom: 8 })
ForEach(this.alerts, (alert) => {
Column() {
Text(alert.message)
.fontSize(16)
.fontColor('#F44336')
Text(alert.suggestion)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 4 })
.padding(8)
})
.padding(16)
.borderRadius(8)
.borderWidth(1)
.borderColor('#F44336')
.width('100%')
.margin({ top: 16 })
}
.padding(16)
private async loadData(): Promise<void> {
const records = await periodRecordService.getRecords();
this.prediction = await predictionService.predictNextPeriod(records);
if (this.prediction) {
this.symptomPredictions = await predictionService.predictSymptoms(this.prediction.nextPeriodStart);
this.alerts = healthAdviceService.checkForAbnormalities(records);
private getSymptomName(type: string): string {
const names: Record<string, string> = {
cramps: '痛经',
headache: '头痛',
bloating: '腹胀',
fatigue: '疲劳',
mood_swings: '情绪波动'
};
return names[type] || type;
private getSeverityColor(severity: number): string {
if (severity >= 4) return '#F44336';
if (severity >= 2) return '#FF9800';
return '#4CAF50';
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return {date.getMonth() + 1}月{date.getDate()}日;
症状记录视图
// SymptomView.ets
@Component
struct SymptomView {
@State records: PeriodRecord[] = [];
@State selectedRecord: PeriodRecord | null = null;
@State selectedSymptom: string = ‘cramps’;
@State symptomSeverity: number = 1;
aboutToAppear() {
this.loadRecords();
EventBus.on(‘periodRecordsUpdated’, () => this.loadRecords());
build() {
Column() {
Text('症状记录')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 16 })
// 选择记录
Picker({ range: this.getRecordOptions(), selected: this.getRecordIndex() })
.onChange((index: number) => {
this.selectedRecord = this.records[index];
})
.margin({ top: 8 })
if (this.selectedRecord) {
// 症状选择
Picker({ range: this.getSymptomOptions(), selected: this.getSymptomIndex() })
.onChange((index: number) => {
this.selectedSymptom = this.getSymptomOptions()[index];
})
.margin({ top: 16 })
// 严重程度选择
Text(严重程度: ${this.symptomSeverity})
.fontSize(16)
.margin({ top: 16 })
Slider({
value: this.symptomSeverity,
min: 1,
max: 5,
step: 1,
style: SliderStyle.SLIDER_OUTSET
})
.onChange((value: number) => {
this.symptomSeverity = value;
})
.margin({ top: 8 })
// 添加症状按钮
Button('记录症状')
.onClick(() => this.addSymptom())
.margin({ top: 16 })
// 已有症状列表
if (this.selectedRecord.symptoms.length > 0) {
Text('已记录的症状')
.fontSize(18)
.margin({ top: 24 })
ForEach(this.selectedRecord.symptoms, (symptom) => {
Row() {
Text(this.getSymptomName(symptom.type))
.fontSize(16)
.layoutWeight(1)
Text(严重程度: ${symptom.severity})
.fontSize(14)
.fontColor(this.getSeverityColor(symptom.severity))
.padding(8)
})
}
.padding(16)
private async loadRecords(): Promise<void> {
this.records = await periodRecordService.getRecords();
if (this.records.length > 0 && !this.selectedRecord) {
this.selectedRecord = this.records[this.records.length - 1];
}
private getRecordOptions(): string[] {
return this.records.map(record =>
经期: {record.startDate}{record.endDate ? 至${record.endDate} : ‘’}
);
private getRecordIndex(): number {
return this.selectedRecord ?
this.records.findIndex(r => r.id === this.selectedRecord!.id) : 0;
private getSymptomOptions(): string[] {
return ['痛经', '头痛', '腹胀', '疲劳', '情绪波动'];
private getSymptomIndex(): number {
const symptoms = this.getSymptomOptions();
const symptomMap: Record<string, string> = {
cramps: '痛经',
headache: '头痛',
bloating: '腹胀',
fatigue: '疲劳',
mood_swings: '情绪波动'
};
const chineseName = symptomMap[this.selectedSymptom] || '痛经';
return symptoms.indexOf(chineseName);
private getSymptomType(chineseName: string): string {
const symptomMap: Record<string, string> = {
'痛经': 'cramps',
'头痛': 'headache',
'腹胀': 'bloating',
'疲劳': 'fatigue',
'情绪波动': 'mood_swings'
};
return symptomMap[chineseName] || 'cramps';
private async addSymptom(): Promise<void> {
if (!this.selectedRecord) return;
const symptom: Symptom = {
type: this.selectedSymptom,
severity: this.symptomSeverity,
recordedAt: Date.now()
};
await periodRecordService.addSymptom(this.selectedRecord.id, symptom);
private getSymptomName(type: string): string {
const names: Record<string, string> = {
cramps: '痛经',
headache: '头痛',
bloating: '腹胀',
fatigue: '疲劳',
mood_swings: '情绪波动'
};
return names[type] || type;
private getSeverityColor(severity: number): string {
if (severity >= 4) return '#F44336';
if (severity >= 2) return '#FF9800';
return '#4CAF50';
}
四、高级功能实现
多设备数据同步
// PeriodSyncService.ets
import deviceManager from ‘@ohos.distributedHardware.deviceManager’;
class PeriodSyncService {
private static instance: PeriodSyncService;
private constructor() {}
public static getInstance(): PeriodSyncService {
if (!PeriodSyncService.instance) {
PeriodSyncService.instance = new PeriodSyncService();
return PeriodSyncService.instance;
public async syncAllDataToDevice(deviceId: string): Promise<void> {
const records = await periodRecordService.getRecords();
const ability = await featureAbility.startAbility({
bundleName: 'com.example.periodTracker',
abilityName: 'PeriodSyncAbility',
deviceId
});
await ability.call({
method: 'receiveAllPeriodData',
parameters: [records]
});
public async syncNewRecordToDevices(record: PeriodRecord): Promise<void> {
const devices = await deviceManager.getTrustedDevices();
await Promise.all(devices.map(device =>
this.sendRecordToDevice(device.id, record)
));
private async sendRecordToDevice(deviceId: string, record: PeriodRecord): Promise<void> {
const ability = await featureAbility.startAbility({
bundleName: 'com.example.periodTracker',
abilityName: 'PeriodRecordAbility',
deviceId
});
await ability.call({
method: 'receivePeriodRecord',
parameters: [record]
});
public async syncPredictionToDevices(prediction: PeriodPrediction): Promise<void> {
const devices = await deviceManager.getTrustedDevices();
await Promise.all(devices.map(device =>
this.sendPredictionToDevice(device.id, prediction)
));
private async sendPredictionToDevice(deviceId: string, prediction: PeriodPrediction): Promise<void> {
const ability = await featureAbility.startAbility({
bundleName: 'com.example.periodTracker',
abilityName: 'PeriodPredictionAbility',
deviceId
});
await ability.call({
method: 'receivePeriodPrediction',
parameters: [prediction]
});
}
export const periodSyncService = PeriodSyncService.getInstance();
智能提醒服务
// PeriodReminderService.ets
import reminderAgent from ‘@ohos.reminderAgent’;
class PeriodReminderService {
private static instance: PeriodReminderService;
private constructor() {}
public static getInstance(): PeriodReminderService {
if (!PeriodReminderService.instance) {
PeriodReminderService.instance = new PeriodReminderService();
return PeriodReminderService.instance;
public async schedulePeriodReminders(prediction: PeriodPrediction): Promise<void> {
await this.cancelAllReminders();
// 经期开始前提醒
const periodReminderTime = new Date(prediction.nextPeriodStart);
periodReminderTime.setDate(periodReminderTime.getDate() - 1); // 提前1天提醒
const periodReminderRequest: reminderAgent.ReminderRequest = {
reminderType: reminderAgent.ReminderType.REMINDER_TYPE_TIMER,
actionButton: [{ title: '已记录' }, { title: '稍后提醒' }],
wantAgent: {
pkgName: 'com.example.periodTracker',
abilityName: 'PeriodReminderAbility'
},
triggerTime: periodReminderTime.getTime(),
title: '经期即将开始',
content: '根据预测,您的经期将于明天开始,请做好准备',
expiredContent: "经期提醒已过期"
};
await reminderAgent.publishReminder(periodReminderRequest);
// 排卵期提醒
const ovulationReminderTime = new Date(prediction.ovulationDate);
const ovulationReminderRequest: reminderAgent.ReminderRequest = {
reminderType: reminderAgent.ReminderType.REMINDER_TYPE_TIMER,
actionButton: [{ title: '知道了' }],
wantAgent: {
pkgName: 'com.example.periodTracker',
abilityName: 'OvulationReminderAbility'
},
triggerTime: ovulationReminderTime.getTime(),
title: '今天是排卵日',
content: '今天是预测的排卵日,易孕期将持续到' +
formatDate(prediction.fertileWindow.end),
expiredContent: "排卵日提醒已过期"
};
await reminderAgent.publishReminder(ovulationReminderRequest);
public async scheduleDailyHealthTips(prediction: PeriodPrediction): Promise<void> {
const now = new Date();
const endDate = new Date(prediction.nextPeriodStart);
endDate.setDate(endDate.getDate() + 30); // 提前安排一个月的提醒
let currentDate = new Date(now);
while (currentDate <= endDate) {
const phase = healthAdviceService.getCyclePhase(
currentDate.toISOString().split('T')[0],
prediction
);
const tips = healthAdviceService.getAdviceForPhase(phase);
if (tips.length > 0) {
const reminderTime = new Date(currentDate);
reminderTime.setHours(10, 0, 0, 0); // 上午10点提醒
const reminderRequest: reminderAgent.ReminderRequest = {
reminderType: reminderAgent.ReminderType.REMINDER_TYPE_TIMER,
actionButton: [{ title: '知道了' }],
wantAgent: {
pkgName: 'com.example.periodTracker',
abilityName: 'HealthTipAbility'
},
triggerTime: reminderTime.getTime(),
title: '今日健康小贴士',
content: tips[0].content,
expiredContent: "健康提醒已过期"
};
await reminderAgent.publishReminder(reminderRequest);
currentDate.setDate(currentDate.getDate() + 1);
}
public async cancelAllReminders(): Promise<void> {
const reminders = await reminderAgent.getValidReminders();
await Promise.all(reminders.map(rem =>
reminderAgent.cancelReminder(rem.id)
));
}
export const periodReminderService = PeriodReminderService.getInstance();
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return {date.getMonth() + 1}月{date.getDate()}日;
健康报告生成
// HealthReportService.ets
class HealthReportService {
private static instance: HealthReportService;
private constructor() {}
public static getInstance(): HealthReportService {
if (!HealthReportService.instance) {
HealthReportService.instance = new HealthReportService();
return HealthReportService.instance;
public async generateMonthlyReport(month: string): Promise<HealthReport> {
const records = await periodRecordService.getRecords();
const monthlyRecords = records.filter(record =>
record.startDate.startsWith(month.substring(0, 7))
);
if (monthlyRecords.length === 0) {
return {
month,
periodLength: 0,
cycleLength: 0,
commonSymptoms: [],
symptomFrequency: {},
abnormalities: []
};
const report: HealthReport = {
month,
periodLength: this.calculateAveragePeriodLength(monthlyRecords),
cycleLength: this.calculateCycleLength(records, month),
commonSymptoms: this.findCommonSymptoms(monthlyRecords),
symptomFrequency: this.calculateSymptomFrequency(monthlyRecords),
abnormalities: healthAdviceService.checkForAbnormalities(monthlyRecords)
};
return report;
private calculateAveragePeriodLength(records: PeriodRecord[]): number {
const lengths = records
.filter(record => record.endDate)
.map(record => {
const start = new Date(record.startDate);
const end = new Date(record.endDate!);
return (end.getTime() - start.getTime()) / (24 60 60 * 1000) + 1;
});
if (lengths.length === 0) return 0;
return lengths.reduce((sum, days) => sum + days, 0) / lengths.length;
private calculateCycleLength(records: PeriodRecord[], month: string): number {
const monthRecords = records.filter(record =>
record.startDate.startsWith(month.substring(0, 7))
);
if (monthRecords.length < 2) return 0;
const prevRecord = records[records.indexOf(monthRecords[0]) - 1];
if (!prevRecord) return 0;
const prevDate = new Date(prevRecord.startDate);
const currentDate = new Date(monthRecords[0].startDate);
return (currentDate.getTime() - prevDate.getTime()) / (24 60 60 * 1000);
private findCommonSymptoms(records: PeriodRecord[]): CommonSymptom[] {
const symptomCount: Record<string, number> = {};
records.forEach(record => {
record.symptoms.forEach(symptom => {
symptomCount[symptom.type] = (symptomCount[symptom.type] || 0) + 1;
});
});
return Object.entries(symptomCount)
.map(([type, count]) => ({
type,
frequency: count / records.length,
averageSeverity: this.calculateAverageSeverity(records, type)
}))
.sort((a, b) => b.frequency - a.frequency);
private calculateSymptomFrequency(records: PeriodRecord[]): Record<string, SymptomStats> {
const stats: Record<string, SymptomStats> = {};
records.forEach(record => {
record.symptoms.forEach(symptom => {
if (!stats[symptom.type]) {
stats[symptom.type] = {
count: 0,
totalSeverity: 0,
days: []
};
stats[symptom.type].count++;
stats[symptom.type].totalSeverity += symptom.severity;
stats[symptom.type].days.push(record.startDate);
});
});
return stats;
private calculateAverageSeverity(records: PeriodRecord[], symptomType: string): number {
const symptoms = records
.flatMap(record => record.symptoms)
.filter(symptom => symptom.type === symptomType);
if (symptoms.length === 0) return 0;
return symptoms.reduce((sum, s) => sum + s.severity, 0) / symptoms.length;
public async generateAnnualReport(year: string): Promise<AnnualHealthReport> {
const records = await periodRecordService.getRecords();
const yearlyRecords = records.filter(record =>
record.startDate.startsWith(year)
);
const monthlyReports: MonthlyReport[] = [];
for (let month = 1; month <= 12; month++) {
const monthStr = month < 10 ? 0{month} : {month};
const monthlyRecords = yearlyRecords.filter(record =>
record.startDate.startsWith({year}-{monthStr})
);
if (monthlyRecords.length > 0) {
monthlyReports.push({
month: {year}-{monthStr},
periodLength: this.calculateAveragePeriodLength(monthlyRecords),
cycleLength: this.calculateCycleLength(yearlyRecords, {year}-{monthStr}),
symptomCount: monthlyRecords.flatMap(r => r.symptoms).length
});
}
return {
year,
averageCycleLength: this.calculateYearlyAverageCycle(yearlyRecords),
averagePeriodLength: this.calculateYearlyAveragePeriod(yearlyRecords),
mostCommonSymptom: this.findYearlyMostCommonSymptom(yearlyRecords),
monthlyReports,
abnormalities: healthAdviceService.checkForAbnormalities(yearlyRecords)
};
private calculateYearlyAverageCycle(records: PeriodRecord[]): number {
if (records.length < 2) return 0;
const cycles: number[] = [];
for (let i = 1; i < records.length; i++) {
const prev = new Date(records[i - 1].startDate);
const current = new Date(records[i].startDate);
cycles.push((current.getTime() - prev.getTime()) / (24 60 60 * 1000));
return cycles.reduce((sum, days) => sum + days, 0) / cycles.length;
private calculateYearlyAveragePeriod(records: PeriodRecord[]): number {
const lengths = records
.filter(record => record.endDate)
.map(record => {
const start = new Date(record.startDate);
const end = new Date(record.endDate!);
return (end.getTime() - start.getTime()) / (24 60 60 * 1000) + 1;
});
if (lengths.length === 0) return 0;
return lengths.reduce((sum, days) => sum + days, 0) / lengths.length;
private findYearlyMostCommonSymptom(records: PeriodRecord[]): CommonSymptom | null {
const symptomCount: Record<string, number> = {};
records.forEach(record => {
record.symptoms.forEach(symptom => {
symptomCount[symptom.type] = (symptomCount[symptom.type] || 0) + 1;
});
});
if (Object.keys(symptomCount).length === 0) return null;
const [type, count] = Object.entries(symptomCount).reduce((max, entry) =>
entry[1] > max[1] ? entry : max
);
return {
type,
frequency: count / records.length,
averageSeverity: this.calculateAverageSeverity(records, type)
};
}
export const healthReportService = HealthReportService.getInstance();
五、总结
本经期预测助手应用实现了以下核心价值:
健康管理:根据周期阶段提供个性化健康建议
异常预警:识别异常周期并提供医疗建议
多设备协同:家庭成员共享健康数据
长期追踪:生成月度和年度健康报告
扩展方向:
集成智能穿戴设备数据
增加备孕模式功能
开发社区分享功能
对接在线医疗服务
注意事项:
预测准确性随记录数据增加而提高
健康建议仅供参考,不能替代专业医疗建议
多设备协同需保持网络连接
首次使用建议至少记录3个月经周期
异常预警需及时咨询专业医生
