鸿蒙经期预测助手应用开发指南 原创

进修的泡芙
发布于 2025-6-22 17:48
浏览
0收藏

鸿蒙经期预测助手应用开发指南

一、系统架构设计

基于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个月经周期

异常预警需及时咨询专业医生

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-6-22 17:54:44修改
收藏
回复
举报
回复
    相关推荐