Flutter实时花粉浓度查询:智能过敏防护助手

项目简介

实时花粉浓度查询是一款专为过敏人群打造的Flutter健康应用,提供全国30个主要城市的实时花粉浓度监测、7天预报和个性化过敏提醒功能。通过科学的花粉指数分析和专业的防护建议,帮助用户有效预防花粉过敏,享受健康生活。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心功能

  • 30个城市覆盖:全国主要城市实时花粉数据
  • 4类花粉监测:树花粉、草花粉、杂草花粉、霉菌孢子
  • 5级指数分类:很低、低、中等、高、很高
  • 实时数据更新:模拟实时花粉浓度监测
  • 7天预报:未来一周花粉浓度趋势
  • 天气关联:结合天气状况分析花粉传播
  • 过敏风险评估:智能评估个人过敏风险等级
  • 防护建议:个性化防护措施推荐
  • 健康提醒:专业的过敏预防指导
  • 紧急处理:过敏急救指南和医院查询

技术特点

  • Material Design 3设计风格
  • NavigationBar底部导航
  • 三页面架构(当前、预报、提醒)
  • 圆形进度指示器
  • LinearProgressIndicator数据可视化
  • 渐变色风险等级展示
  • 响应式网格布局
  • 模态底部表单
  • 模拟实时数据更新
  • 无需额外依赖包

核心代码实现

1. 花粉数据模型

class PollenData {
  final String id;                    // 数据ID
  final String cityName;              // 城市名称
  final String province;              // 省份
  final DateTime updateTime;          // 更新时间
  final int overallIndex;             // 综合花粉指数
  final Map<String, int> pollenTypes; // 分类花粉指数
  final String weatherCondition;      // 天气状况
  final double temperature;           // 温度
  final int humidity;                 // 湿度
  final double windSpeed;             // 风速
  final String windDirection;         // 风向
  final List<String> allergyTips;     // 过敏提醒
  final List<DailyForecast> forecast; // 7天预报

  PollenData({
    required this.id,
    required this.cityName,
    required this.province,
    required this.updateTime,
    required this.overallIndex,
    required this.pollenTypes,
    required this.weatherCondition,
    required this.temperature,
    required this.humidity,
    required this.windSpeed,
    required this.windDirection,
    required this.allergyTips,
    required this.forecast,
  });

  // 计算属性:完整地址
  String get location => '$province $cityName';

  // 计算属性:花粉等级
  String get overallLevel {
    if (overallIndex <= 20) return '很低';
    if (overallIndex <= 40) return '低';
    if (overallIndex <= 60) return '中等';
    if (overallIndex <= 80) return '高';
    return '很高';
  }

  // 计算属性:等级颜色
  Color get overallColor {
    if (overallIndex <= 20) return Colors.green;
    if (overallIndex <= 40) return Colors.lightGreen;
    if (overallIndex <= 60) return Colors.orange;
    if (overallIndex <= 80) return Colors.deepOrange;
    return Colors.red;
  }

  // 计算属性:天气图标
  IconData get weatherIcon {
    switch (weatherCondition) {
      case '晴天': return Icons.wb_sunny;
      case '多云': return Icons.wb_cloudy;
      case '阴天': return Icons.cloud;
      case '小雨': return Icons.grain;
      case '中雨': return Icons.umbrella;
      case '大雨': return Icons.thunderstorm;
      default: return Icons.wb_sunny;
    }
  }

  // 计算属性:更新时间文本
  String get updateTimeText {
    final now = DateTime.now();
    final diff = now.difference(updateTime);
    if (diff.inMinutes < 1) return '刚刚更新';
    if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前更新';
    if (diff.inHours < 24) return '${diff.inHours}小时前更新';
    return '${diff.inDays}天前更新';
  }
}

模型字段说明

字段 类型 说明
id String 唯一标识符
cityName String 城市名称
province String 所在省份
updateTime DateTime 数据更新时间
overallIndex int 综合花粉指数(0-100)
pollenTypes Map<String, int> 分类花粉指数
weatherCondition String 天气状况
temperature double 当前温度
humidity int 湿度百分比
windSpeed double 风速(m/s)
windDirection String 风向
allergyTips List 防护建议列表
forecast List 7天预报数据

计算属性

  • location:组合省份和城市名称
  • overallLevel:根据指数返回等级文本
  • overallColor:根据指数返回对应颜色
  • weatherIcon:根据天气返回对应图标
  • updateTimeText:格式化更新时间显示

花粉指数等级划分

指数范围 等级 颜色 说明
0-20 很低 绿色 过敏风险很低
21-40 浅绿 过敏风险较低
41-60 中等 橙色 过敏风险中等
61-80 深橙 过敏风险较高
81-100 很高 红色 过敏风险很高

2. 预报数据模型

class DailyForecast {
  final DateTime date;        // 日期
  final int pollenIndex;      // 花粉指数
  final String weather;       // 天气
  final double maxTemp;       // 最高温度
  final double minTemp;       // 最低温度

  DailyForecast({
    required this.date,
    required this.pollenIndex,
    required this.weather,
    required this.maxTemp,
    required this.minTemp,
  });

  // 计算属性:花粉等级
  String get level {
    if (pollenIndex <= 20) return '很低';
    if (pollenIndex <= 40) return '低';
    if (pollenIndex <= 60) return '中等';
    if (pollenIndex <= 80) return '高';
    return '很高';
  }

  // 计算属性:等级颜色
  Color get levelColor {
    if (pollenIndex <= 20) return Colors.green;
    if (pollenIndex <= 40) return Colors.lightGreen;
    if (pollenIndex <= 60) return Colors.orange;
    if (pollenIndex <= 80) return Colors.deepOrange;
    return Colors.red;
  }

  // 计算属性:日期文本
  String get dateText {
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final targetDate = DateTime(date.year, date.month, date.day);
    final diff = targetDate.difference(today).inDays;
    
    if (diff == 0) return '今天';
    if (diff == 1) return '明天';
    if (diff == 2) return '后天';
    return '${date.month}${date.day}日';
  }
}

预报数据特点

  • 7天预报数据
  • 包含花粉指数和天气信息
  • 智能日期显示(今天、明天、后天)
  • 温度范围显示
  • 等级颜色映射

3. 花粉数据生成

void _generatePollenData() {
  final random = Random();
  
  final pollenTypes = ['树花粉', '草花粉', '杂草花粉', '霉菌孢子'];
  final weatherConditions = ['晴天', '多云', '阴天', '小雨', '中雨'];
  final windDirections = ['北风', '南风', '东风', '西风', '东北风', '西北风', '东南风', '西南风'];
  
  final allergyTipsList = [
    ['减少户外活动时间', '外出佩戴口罩', '关闭门窗', '使用空气净化器'],
    ['避免在花粉高峰期外出', '回家后及时洗手洗脸', '更换外出衣物', '保持室内湿度'],
    ['服用抗过敏药物', '避免接触过敏原', '多喝水', '注意休息'],
    ['及时就医', '随身携带急救药物', '避免剧烈运动', '保持心情舒畅'],
  ];

  for (String city in _cities) {
    final overallIndex = 10 + random.nextInt(80);
    final pollenTypeData = <String, int>{};
    
    // 生成各类花粉指数
    for (String type in pollenTypes) {
      pollenTypeData[type] = 5 + random.nextInt(90);
    }

    // 生成7天预报
    final forecast = <DailyForecast>[];
    for (int i = 0; i < 7; i++) {
      forecast.add(DailyForecast(
        date: DateTime.now().add(Duration(days: i)),
        pollenIndex: 10 + random.nextInt(80),
        weather: weatherConditions[random.nextInt(weatherConditions.length)],
        maxTemp: 15 + random.nextDouble() * 20,
        minTemp: 5 + random.nextDouble() * 15,
      ));
    }

    _allPollenData.add(PollenData(
      id: 'pollen_${city.hashCode}',
      cityName: city,
      province: _cityProvinces[city]!,
      updateTime: DateTime.now().subtract(Duration(minutes: random.nextInt(60))),
      overallIndex: overallIndex,
      pollenTypes: pollenTypeData,
      weatherCondition: weatherConditions[random.nextInt(weatherConditions.length)],
      temperature: 10 + random.nextDouble() * 25,
      humidity: 30 + random.nextInt(50),
      windSpeed: random.nextDouble() * 10,
      windDirection: windDirections[random.nextInt(windDirections.length)],
      allergyTips: allergyTipsList[random.nextInt(allergyTipsList.length)],
      forecast: forecast,
    ));
  }
}

数据生成特点

  1. 30个主要城市数据覆盖
  2. 4种花粉类型随机生成
  3. 综合指数范围:10-90
  4. 天气状况:5种类型随机
  5. 风向:8个方向随机
  6. 防护建议:4套方案随机选择
  7. 7天预报:每天独立生成
  8. 更新时间:0-60分钟前随机

城市覆盖(30个):

  • 直辖市:北京、上海、天津、重庆
  • 省会城市:广州、杭州、南京、武汉、成都、西安等
  • 重要城市:深圳、苏州、青岛、大连、宁波、厦门等

4. NavigationBar底部导航

bottomNavigationBar: NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() => _selectedIndex = index);
  },
  destinations: const [
    NavigationDestination(icon: Icon(Icons.home), label: '当前'),
    NavigationDestination(icon: Icon(Icons.calendar_today), label: '预报'),
    NavigationDestination(icon: Icon(Icons.health_and_safety), label: '提醒'),
  ],
),

三个页面

页面 图标 功能
当前 home 显示当前城市实时花粉数据
预报 calendar_today 显示7天花粉浓度预报
提醒 health_and_safety 显示过敏风险和防护建议

IndexedStack使用

IndexedStack(
  index: _selectedIndex,
  children: [
    _buildCurrentPage(),
    _buildForecastPage(),
    _buildTipsPage(),
  ],
),

5. 综合花粉指数展示

Widget _buildOverallIndexCard() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          Row(
            children: [
              Icon(Icons.eco, color: _currentPollenData!.overallColor),
              const SizedBox(width: 8),
              const Text(
                '综合花粉指数',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 20),
          Container(
            width: 120,
            height: 120,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: _currentPollenData!.overallColor.withValues(alpha: 0.1),
              border: Border.all(
                color: _currentPollenData!.overallColor,
                width: 4,
              ),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  '${_currentPollenData!.overallIndex}',
                  style: TextStyle(
                    fontSize: 36,
                    fontWeight: FontWeight.bold,
                    color: _currentPollenData!.overallColor,
                  ),
                ),
                Text(
                  _currentPollenData!.overallLevel,
                  style: TextStyle(
                    fontSize: 14,
                    color: _currentPollenData!.overallColor,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),
          _buildIndexScale(),
        ],
      ),
    ),
  );
}

圆形指数展示特点

  • 120x120像素圆形容器
  • 边框颜色根据等级动态变化
  • 背景色透明度0.1
  • 中心显示数值和等级文本
  • 底部显示等级刻度

等级刻度实现

Widget _buildIndexScale() {
  return Column(
    children: [
      const Text(
        '花粉指数等级',
        style: TextStyle(fontSize: 12, color: Colors.grey),
      ),
      const SizedBox(height: 8),
      Row(
        children: [
          Expanded(child: _buildScaleItem('很低', '0-20', Colors.green)),
          Expanded(child: _buildScaleItem('低', '21-40', Colors.lightGreen)),
          Expanded(child: _buildScaleItem('中等', '41-60', Colors.orange)),
          Expanded(child: _buildScaleItem('高', '61-80', Colors.deepOrange)),
          Expanded(child: _buildScaleItem('很高', '81-100', Colors.red)),
        ],
      ),
    ],
  );
}

Widget _buildScaleItem(String level, String range, Color color) {
  final isActive = _currentPollenData!.overallLevel == level;
  return Container(
    margin: const EdgeInsets.symmetric(horizontal: 2),
    padding: const EdgeInsets.symmetric(vertical: 8),
    decoration: BoxDecoration(
      color: isActive ? color.withValues(alpha: 0.2) : Colors.grey.withValues(alpha: 0.1),
      borderRadius: BorderRadius.circular(8),
      border: isActive ? Border.all(color: color, width: 2) : null,
    ),
    child: Column(
      children: [
        Text(
          level,
          style: TextStyle(
            fontSize: 10,
            fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
            color: isActive ? color : Colors.grey,
          ),
        ),
        Text(
          range,
          style: TextStyle(
            fontSize: 8,
            color: isActive ? color : Colors.grey,
          ),
        ),
      ],
    ),
  );
}

6. 分类花粉指数展示

Widget _buildPollenTypesCard() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Icon(Icons.grass, color: Colors.green),
              SizedBox(width: 8),
              Text(
                '分类花粉指数',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 16),
          ..._currentPollenData!.pollenTypes.entries.map((entry) {
            return Padding(
              padding: const EdgeInsets.only(bottom: 12),
              child: _buildPollenTypeItem(entry.key, entry.value),
            );
          }),
        ],
      ),
    ),
  );
}

Widget _buildPollenTypeItem(String type, int value) {
  Color color;
  String level;
  
  if (value <= 20) {
    color = Colors.green;
    level = '很低';
  } else if (value <= 40) {
    color = Colors.lightGreen;
    level = '低';
  } else if (value <= 60) {
    color = Colors.orange;
    level = '中等';
  } else if (value <= 80) {
    color = Colors.deepOrange;
    level = '高';
  } else {
    color = Colors.red;
    level = '很高';
  }

  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(type, style: const TextStyle(fontSize: 14)),
          Row(
            children: [
              Text(
                '$value',
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: color,
                ),
              ),
              const SizedBox(width: 8),
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                decoration: BoxDecoration(
                  color: color.withValues(alpha: 0.2),
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Text(
                  level,
                  style: TextStyle(
                    fontSize: 10,
                    color: color,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
      const SizedBox(height: 4),
      LinearProgressIndicator(
        value: value / 100,
        backgroundColor: Colors.grey[200],
        color: color,
        minHeight: 6,
      ),
    ],
  );
}

分类花粉展示特点

  • 4种花粉类型:树花粉、草花粉、杂草花粉、霉菌孢子
  • 每种花粉独立显示指数和等级
  • LinearProgressIndicator可视化进度
  • 动态颜色和等级标签
  • 进度条高度6px

7. 天气状况卡片

Widget _buildWeatherCard() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Icon(Icons.wb_sunny, color: Colors.orange),
              SizedBox(width: 8),
              Text(
                '天气状况',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Icon(
                _currentPollenData!.weatherIcon,
                size: 48,
                color: Colors.orange,
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      _currentPollenData!.weatherCondition,
                      style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                    Text(
                      '${_currentPollenData!.temperature.toStringAsFixed(1)}°C',
                      style: const TextStyle(fontSize: 16, color: Colors.grey),
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(
                child: _buildWeatherItem(
                  '湿度',
                  '${_currentPollenData!.humidity}%',
                  Icons.water_drop,
                  Colors.blue,
                ),
              ),
              Expanded(
                child: _buildWeatherItem(
                  '风速',
                  '${_currentPollenData!.windSpeed.toStringAsFixed(1)}m/s',
                  Icons.air,
                  Colors.grey,
                ),
              ),
              Expanded(
                child: _buildWeatherItem(
                  '风向',
                  _currentPollenData!.windDirection,
                  Icons.navigation,
                  Colors.green,
                ),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

Widget _buildWeatherItem(String label, String value, IconData icon, Color color) {
  return Container(
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: color.withValues(alpha: 0.1),
      borderRadius: BorderRadius.circular(8),
    ),
    child: Column(
      children: [
        Icon(icon, color: color, size: 24),
        const SizedBox(height: 4),
        Text(
          value,
          style: TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
        Text(
          label,
          style: const TextStyle(fontSize: 10, color: Colors.grey),
        ),
      ],
    ),
  );
}

天气信息展示

  • 大图标显示天气状况
  • 温度精确到小数点后1位
  • 湿度、风速、风向三项指标
  • 每项指标独立的颜色主题
  • 圆角背景容器

8. 7天预报页面

Widget _buildForecastPage() {
  if (_currentPollenData == null) {
    return const Center(child: Text('暂无预报数据'));
  }

  return ListView(
    padding: const EdgeInsets.all(16),
    children: [
      Card(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  const Icon(Icons.calendar_today, color: Colors.blue),
                  const SizedBox(width: 8),
                  Text(
                    '${_currentPollenData!.cityName} 7天花粉预报',
                    style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
              const SizedBox(height: 16),
              ..._currentPollenData!.forecast.map((forecast) {
                return _buildForecastItem(forecast);
              }),
            ],
          ),
        ),
      ),
    ],
  );
}

Widget _buildForecastItem(DailyForecast forecast) {
  return Container(
    margin: const EdgeInsets.only(bottom: 12),
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.grey.withValues(alpha: 0.05),
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
    ),
    child: Row(
      children: [
        SizedBox(
          width: 60,
          child: Text(
            forecast.dateText,
            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
          ),
        ),
        const SizedBox(width: 12),
        Icon(
          _getWeatherIcon(forecast.weather),
          color: Colors.orange,
          size: 24,
        ),
        const SizedBox(width: 8),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                forecast.weather,
                style: const TextStyle(fontSize: 12),
              ),
              Text(
                '${forecast.maxTemp.toStringAsFixed(0)}°/${forecast.minTemp.toStringAsFixed(0)}°',
                style: TextStyle(fontSize: 10, color: Colors.grey[600]),
              ),
            ],
          ),
        ),
        const SizedBox(width: 12),
        Container(
          width: 60,
          height: 30,
          decoration: BoxDecoration(
            color: forecast.levelColor.withValues(alpha: 0.1),
            borderRadius: BorderRadius.circular(15),
            border: Border.all(color: forecast.levelColor, width: 1),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                '${forecast.pollenIndex}',
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.bold,
                  color: forecast.levelColor,
                ),
              ),
              Text(
                forecast.level,
                style: TextStyle(
                  fontSize: 8,
                  color: forecast.levelColor,
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

预报页面特点

  • 7天完整预报数据
  • 日期智能显示(今天、明天、后天)
  • 天气图标和温度范围
  • 花粉指数胶囊式展示
  • 等级颜色动态变化

9. 过敏风险评估

Widget _buildAllergyLevelCard() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(
                Icons.warning,
                color: _currentPollenData!.overallColor,
              ),
              const SizedBox(width: 8),
              const Text(
                '过敏风险等级',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: _currentPollenData!.overallColor.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(12),
              border: Border.all(
                color: _currentPollenData!.overallColor,
                width: 2,
              ),
            ),
            child: Column(
              children: [
                Icon(
                  _getRiskIcon(_currentPollenData!.overallLevel),
                  size: 48,
                  color: _currentPollenData!.overallColor,
                ),
                const SizedBox(height: 8),
                Text(
                  _currentPollenData!.overallLevel,
                  style: TextStyle(
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                    color: _currentPollenData!.overallColor,
                  ),
                ),
                Text(
                  _getRiskDescription(_currentPollenData!.overallLevel),
                  style: TextStyle(
                    fontSize: 14,
                    color: _currentPollenData!.overallColor,
                  ),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

IconData _getRiskIcon(String level) {
  switch (level) {
    case '很低': return Icons.sentiment_very_satisfied;
    case '低': return Icons.sentiment_satisfied;
    case '中等': return Icons.sentiment_neutral;
    case '高': return Icons.sentiment_dissatisfied;
    case '很高': return Icons.sentiment_very_dissatisfied;
    default: return Icons.sentiment_neutral;
  }
}

String _getRiskDescription(String level) {
  switch (level) {
    case '很低': return '过敏风险很低,可以正常户外活动';
    case '低': return '过敏风险较低,敏感人群需注意';
    case '中等': return '过敏风险中等,建议减少户外活动';
    case '高': return '过敏风险较高,敏感人群避免外出';
    case '很高': return '过敏风险很高,建议待在室内';
    default: return '请注意防护';
  }
}

风险评估特点

  • 表情图标直观显示风险等级
  • 大号文字突出风险等级
  • 详细的风险描述说明
  • 边框和背景色动态变化
  • 全宽度容器展示

风险等级映射

等级 图标 描述
很低 sentiment_very_satisfied 可以正常户外活动
sentiment_satisfied 敏感人群需注意
中等 sentiment_neutral 建议减少户外活动
sentiment_dissatisfied 敏感人群避免外出
很高 sentiment_very_dissatisfied 建议待在室内

10. 防护建议展示

Widget _buildAllergyTipsCard() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Icon(Icons.lightbulb, color: Colors.amber),
              SizedBox(width: 8),
              Text(
                '防护建议',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 12),
          ..._currentPollenData!.allergyTips.asMap().entries.map((entry) {
            return Padding(
              padding: const EdgeInsets.only(bottom: 8),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Container(
                    width: 20,
                    height: 20,
                    decoration: const BoxDecoration(
                      color: Colors.amber,
                      shape: BoxShape.circle,
                    ),
                    child: Center(
                      child: Text(
                        '${entry.key + 1}',
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 12,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      entry.value,
                      style: const TextStyle(fontSize: 14, height: 1.5),
                    ),
                  ),
                ],
              ),
            );
          }),
        ],
      ),
    ),
  );
}

防护建议特点

  • 编号圆形标识
  • 琥珀色主题
  • 行高1.5提升可读性
  • 动态建议内容
  • 列表式展示

建议类型(4套方案):

  1. 基础防护:减少户外活动、佩戴口罩、关闭门窗、使用净化器
  2. 进阶防护:避开高峰期、及时清洁、更换衣物、保持湿度
  3. 药物防护:服用抗过敏药、避免过敏原、多喝水、注意休息
  4. 高级防护:及时就医、携带急救药、避免运动、保持心情

11. 健康建议卡片

Widget _buildHealthAdviceCard() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Icon(Icons.health_and_safety, color: Colors.green),
              SizedBox(width: 8),
              Text(
                '健康建议',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 12),
          _buildAdviceItem(
            Icons.masks,
            '佩戴防护',
            '外出时佩戴N95口罩或防花粉口罩',
            Colors.blue,
          ),
          _buildAdviceItem(
            Icons.home,
            '室内防护',
            '关闭门窗,使用空气净化器',
            Colors.green,
          ),
          _buildAdviceItem(
            Icons.local_hospital,
            '药物准备',
            '备好抗过敏药物,如有不适及时服用',
            Colors.orange,
          ),
          _buildAdviceItem(
            Icons.schedule,
            '时间选择',
            '避开花粉浓度高峰期(上午6-10点)',
            Colors.purple,
          ),
        ],
      ),
    ),
  );
}

Widget _buildAdviceItem(IconData icon, String title, String content, Color color) {
  return Padding(
    padding: const EdgeInsets.only(bottom: 12),
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Container(
          padding: const EdgeInsets.all(8),
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.1),
            borderRadius: BorderRadius.circular(8),
          ),
          child: Icon(icon, color: color, size: 20),
        ),
        const SizedBox(width: 12),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                title,
                style: const TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 4),
              Text(
                content,
                style: TextStyle(
                  fontSize: 12,
                  color: Colors.grey[600],
                  height: 1.4,
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

健康建议特点

  • 4个专业建议类别
  • 图标和颜色主题区分
  • 标题和详细说明
  • 圆角图标背景
  • 行高1.4优化阅读

建议分类

  1. 佩戴防护:蓝色,口罩图标
  2. 室内防护:绿色,房屋图标
  3. 药物准备:橙色,医院图标
  4. 时间选择:紫色,时间图标

12. 紧急情况处理

Widget _buildEmergencyCard() {
  return Card(
    color: Colors.red.withValues(alpha: 0.05),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Row(
            children: [
              Icon(Icons.emergency, color: Colors.red),
              SizedBox(width: 8),
              Text(
                '紧急情况处理',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.red.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(8),
              border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  '如出现以下症状,请立即就医:',
                  style: TextStyle(
                    fontSize: 14,
                    fontWeight: FontWeight.bold,
                    color: Colors.red,
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  '• 呼吸困难、胸闷\n• 严重皮疹、全身瘙痒\n• 眼睛红肿、流泪不止\n• 持续打喷嚏、流鼻涕',
                  style: TextStyle(fontSize: 12, height: 1.5),
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      child: ElevatedButton.icon(
                        onPressed: () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('正在拨打急救电话...')),
                          );
                        },
                        icon: const Icon(Icons.phone, color: Colors.white),
                        label: const Text('急救电话', style: TextStyle(color: Colors.white)),
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.red,
                          foregroundColor: Colors.white,
                        ),
                      ),
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: OutlinedButton.icon(
                        onPressed: () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('正在查找附近医院...')),
                          );
                        },
                        icon: const Icon(Icons.local_hospital, color: Colors.red),
                        label: const Text('附近医院', style: TextStyle(color: Colors.red)),
                        style: OutlinedButton.styleFrom(
                          side: const BorderSide(color: Colors.red),
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

紧急处理特点

  • 红色主题突出紧急性
  • 详细的症状描述
  • 两个操作按钮
  • 边框和背景强调
  • 实心和空心按钮区分

紧急症状

  • 呼吸困难、胸闷
  • 严重皮疹、全身瘙痒
  • 眼睛红肿、流泪不止
  • 持续打喷嚏、流鼻涕

13. 城市选择器

void _showCitySelector() {
  showModalBottomSheet(
    context: context,
    builder: (context) => Container(
      height: 400,
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '选择城市',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Expanded(
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                childAspectRatio: 2.5,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              itemCount: _cities.length,
              itemBuilder: (context, index) {
                final city = _cities[index];
                final isSelected = city == _selectedCity;
                return InkWell(
                  onTap: () {
                    setState(() {
                      _selectedCity = city;
                    });
                    Navigator.pop(context);
                    _loadCurrentCityData();
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      color: isSelected
                          ? Colors.green.withValues(alpha: 0.1)
                          : Colors.grey.withValues(alpha: 0.1),
                      borderRadius: BorderRadius.circular(8),
                      border: isSelected
                          ? Border.all(color: Colors.green, width: 2)
                          : Border.all(color: Colors.grey.withValues(alpha: 0.3)),
                    ),
                    child: Center(
                      child: Text(
                        city,
                        style: TextStyle(
                          fontSize: 12,
                          fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                          color: isSelected ? Colors.green : Colors.black,
                        ),
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    ),
  );
}

城市选择器特点

  • 模态底部表单
  • 3列网格布局
  • 宽高比2.5:1
  • 选中状态高亮
  • 点击切换城市并刷新数据

14. 数据刷新功能

void _loadCurrentCityData() {
  setState(() {
    _isLoading = true;
  });

  // 模拟网络请求延迟
  Future.delayed(const Duration(seconds: 1), () {
    setState(() {
      _currentPollenData = _allPollenData.firstWhere(
        (data) => data.cityName == _selectedCity,
      );
      _isLoading = false;
    });
  });
}

void _refreshData() {
  _generatePollenData();
  _loadCurrentCityData();
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('数据已刷新')),
  );
}

刷新功能特点

  • 模拟1秒网络延迟
  • 加载状态指示器
  • 重新生成所有数据
  • SnackBar反馈提示
  • 自动切换到当前城市数据

技术要点详解

1. 圆形进度指示器

圆形进度指示器用于直观展示花粉指数等级。

实现要点

Container(
  width: 120,
  height: 120,
  decoration: BoxDecoration(
    shape: BoxShape.circle,
    color: color.withValues(alpha: 0.1),
    border: Border.all(color: color, width: 4),
  ),
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Text('${index}', style: TextStyle(fontSize: 36, color: color)),
      Text(level, style: TextStyle(fontSize: 14, color: color)),
    ],
  ),
)

设计特点

  • 固定尺寸120x120像素
  • 圆形边框,宽度4像素
  • 背景色透明度0.1
  • 中心垂直居中布局
  • 数值和等级文本分层显示

2. LinearProgressIndicator数据可视化

线性进度条用于展示分类花粉指数。

基本用法

LinearProgressIndicator(
  value: value / 100,           // 进度值(0.0-1.0)
  backgroundColor: Colors.grey[200],
  color: color,                 // 进度条颜色
  minHeight: 6,                 // 最小高度
)

属性说明

  • value:进度值,范围0.0-1.0
  • backgroundColor:背景颜色
  • color:进度条颜色
  • minHeight:进度条高度

使用场景

  • 花粉指数可视化
  • 数据百分比展示
  • 等级进度显示

3. 动态颜色映射

根据数值动态返回对应颜色。

实现方式

Color getColorByValue(int value) {
  if (value <= 20) return Colors.green;
  if (value <= 40) return Colors.lightGreen;
  if (value <= 60) return Colors.orange;
  if (value <= 80) return Colors.deepOrange;
  return Colors.red;
}

应用场景

  • 花粉指数等级颜色
  • 风险等级标识
  • 状态指示器
  • 数据可视化

4. 模态底部表单

showModalBottomSheet用于显示城市选择器。

基本用法

showModalBottomSheet(
  context: context,
  builder: (context) => Container(
    height: 400,
    child: // 内容
  ),
)

特点

  • 从底部弹出
  • 模态覆盖
  • 可拖拽关闭
  • 自定义高度
  • 圆角设计

5. GridView网格布局

GridView.builder用于城市选择网格。

配置参数

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,        // 列数
    childAspectRatio: 2.5,    // 宽高比
    crossAxisSpacing: 8,      // 列间距
    mainAxisSpacing: 8,       // 行间距
  ),
  itemCount: items.length,
  itemBuilder: (context, index) => // 构建项
)

使用场景

  • 城市选择器
  • 图片网格
  • 按钮组
  • 标签展示

6. 计算属性优化

使用Getter实现动态计算属性。

优势

  • 减少存储空间
  • 保持数据一致性
  • 简化代码逻辑
  • 便于维护

示例

class PollenData {
  final int overallIndex;
  
  String get overallLevel {
    if (overallIndex <= 20) return '很低';
    if (overallIndex <= 40) return '低';
    // ...
  }
  
  Color get overallColor {
    if (overallIndex <= 20) return Colors.green;
    // ...
  }
}

7. 时间格式化

智能显示相对时间和日期。

更新时间格式化

String get updateTimeText {
  final now = DateTime.now();
  final diff = now.difference(updateTime);
  if (diff.inMinutes < 1) return '刚刚更新';
  if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前更新';
  if (diff.inHours < 24) return '${diff.inHours}小时前更新';
  return '${diff.inDays}天前更新';
}

日期格式化

String get dateText {
  final now = DateTime.now();
  final today = DateTime(now.year, now.month, now.day);
  final targetDate = DateTime(date.year, date.month, date.day);
  final diff = targetDate.difference(today).inDays;
  
  if (diff == 0) return '今天';
  if (diff == 1) return '明天';
  if (diff == 2) return '后天';
  return '${date.month}${date.day}日';
}

8. 状态管理

使用setState管理应用状态。

状态变量

int _selectedIndex = 0;           // 当前页面索引
String _selectedCity = '北京市';   // 选中城市
List<PollenData> _allPollenData = []; // 所有花粉数据
PollenData? _currentPollenData;   // 当前城市数据
bool _isLoading = false;          // 加载状态

状态更新

void _updateCity(String city) {
  setState(() {
    _selectedCity = city;
    _isLoading = true;
  });
  _loadCurrentCityData();
}

9. 异步数据加载

模拟网络请求和数据加载。

加载流程

void _loadCurrentCityData() {
  setState(() => _isLoading = true);
  
  Future.delayed(const Duration(seconds: 1), () {
    setState(() {
      _currentPollenData = _allPollenData.firstWhere(
        (data) => data.cityName == _selectedCity,
      );
      _isLoading = false;
    });
  });
}

加载状态显示

if (_isLoading) {
  return const Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CircularProgressIndicator(),
        SizedBox(height: 16),
        Text('正在获取花粉数据...'),
      ],
    ),
  );
}

10. 用户反馈

使用SnackBar提供操作反馈。

基本用法

ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(
    content: Text('数据已刷新'),
    duration: Duration(seconds: 2),
  ),
);

应用场景

  • 数据刷新提示
  • 操作成功反馈
  • 错误信息提示
  • 功能说明

功能扩展方向

1. 实时数据接入

API接口集成

class PollenApiService {
  static const String baseUrl = 'https://api.pollen.gov.cn';
  
  // 获取实时花粉数据
  static Future<PollenData> getRealTimeData(String cityCode) async {
    final response = await http.get(
      Uri.parse('$baseUrl/realtime/$cityCode'),
      headers: {'Authorization': 'Bearer YOUR_API_KEY'},
    );
    
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return PollenData.fromJson(data);
    }
    throw Exception('Failed to load pollen data');
  }
  
  // 获取7天预报
  static Future<List<DailyForecast>> getForecast(String cityCode) async {
    final response = await http.get(
      Uri.parse('$baseUrl/forecast/$cityCode'),
    );
    
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return (data['forecast'] as List)
          .map((json) => DailyForecast.fromJson(json))
          .toList();
    }
    throw Exception('Failed to load forecast data');
  }
  
  // 获取历史数据
  static Future<List<HistoricalData>> getHistoricalData({
    required String cityCode,
    required DateTime startDate,
    required DateTime endDate,
  }) async {
    final response = await http.get(
      Uri.parse('$baseUrl/historical/$cityCode').replace(
        queryParameters: {
          'start': startDate.toIso8601String(),
          'end': endDate.toIso8601String(),
        },
      ),
    );
    
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      return (data['historical'] as List)
          .map((json) => HistoricalData.fromJson(json))
          .toList();
    }
    throw Exception('Failed to load historical data');
  }
}

数据缓存策略

class PollenDataCache {
  static const Duration cacheExpiry = Duration(minutes: 30);
  static final Map<String, CachedData> _cache = {};
  
  static Future<PollenData?> getCachedData(String cityCode) async {
    final cached = _cache[cityCode];
    if (cached != null && !cached.isExpired) {
      return cached.data;
    }
    return null;
  }
  
  static void setCachedData(String cityCode, PollenData data) {
    _cache[cityCode] = CachedData(
      data: data,
      timestamp: DateTime.now(),
    );
  }
  
  static void clearExpiredCache() {
    _cache.removeWhere((key, value) => value.isExpired);
  }
}

class CachedData {
  final PollenData data;
  final DateTime timestamp;
  
  CachedData({required this.data, required this.timestamp});
  
  bool get isExpired {
    return DateTime.now().difference(timestamp) > PollenDataCache.cacheExpiry;
  }
}

2. 个性化过敏档案

用户过敏档案

class AllergyProfile {
  final String userId;
  final List<String> allergens;      // 过敏原列表
  final int sensitivityLevel;        // 敏感度等级(1-5)
  final List<String> symptoms;       // 常见症状
  final List<String> medications;    // 常用药物
  final String doctorAdvice;         // 医生建议
  final DateTime lastUpdated;        // 最后更新时间
  
  AllergyProfile({
    required this.userId,
    required this.allergens,
    required this.sensitivityLevel,
    required this.symptoms,
    required this.medications,
    required this.doctorAdvice,
    required this.lastUpdated,
  });
  
  // 计算个人风险等级
  String getPersonalRiskLevel(PollenData pollenData) {
    int riskScore = 0;
    
    // 基础风险分数
    riskScore += pollenData.overallIndex;
    
    // 个人敏感度调整
    riskScore = (riskScore * (sensitivityLevel / 3.0)).round();
    
    // 特定过敏原调整
    for (String allergen in allergens) {
      if (pollenData.pollenTypes.containsKey(allergen)) {
        riskScore += (pollenData.pollenTypes[allergen]! * 0.5).round();
      }
    }
    
    // 天气因素调整
    if (pollenData.weatherCondition == '晴天' && pollenData.windSpeed > 3) {
      riskScore += 10; // 晴天大风增加风险
    }
    if (pollenData.weatherCondition == '小雨') {
      riskScore -= 15; // 小雨降低风险
    }
    
    if (riskScore <= 30) return '很低';
    if (riskScore <= 50) return '低';
    if (riskScore <= 70) return '中等';
    if (riskScore <= 90) return '高';
    return '很高';
  }
  
  // 获取个性化建议
  List<String> getPersonalizedTips(PollenData pollenData) {
    List<String> tips = [];
    
    final riskLevel = getPersonalRiskLevel(pollenData);
    
    switch (riskLevel) {
      case '很高':
        tips.addAll([
          '建议今日不要外出',
          '如需外出请佩戴专业防花粉口罩',
          '准备好${medications.join('')}等药物',
          '保持室内空气净化器开启',
        ]);
        break;
      case '高':
        tips.addAll([
          '尽量减少户外活动时间',
          '外出时佩戴口罩和护目镜',
          '避开上午6-10点花粉高峰期',
        ]);
        break;
      case '中等':
        tips.addAll([
          '外出时注意防护',
          '回家后及时清洗面部和手部',
          '关闭门窗,使用空调循环',
        ]);
        break;
      default:
        tips.addAll([
          '可以正常户外活动',
          '敏感人群仍需适当注意',
        ]);
    }
    
    return tips;
  }
}

class AllergyProfilePage extends StatefulWidget {
  
  State<AllergyProfilePage> createState() => _AllergyProfilePageState();
}

class _AllergyProfilePageState extends State<AllergyProfilePage> {
  final _formKey = GlobalKey<FormState>();
  List<String> _selectedAllergens = [];
  int _sensitivityLevel = 3;
  List<String> _symptoms = [];
  List<String> _medications = [];
  String _doctorAdvice = '';
  
  final List<String> _availableAllergens = [
    '树花粉', '草花粉', '杂草花粉', '霉菌孢子', '尘螨', '动物毛发'
  ];
  
  final List<String> _availableSymptoms = [
    '打喷嚏', '流鼻涕', '鼻塞', '眼睛痒', '流眼泪', '皮肤过敏', '咳嗽', '气喘'
  ];
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('过敏档案')),
      body: Form(
        key: _formKey,
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildAllergenSelection(),
            const SizedBox(height: 16),
            _buildSensitivitySlider(),
            const SizedBox(height: 16),
            _buildSymptomSelection(),
            const SizedBox(height: 16),
            _buildMedicationInput(),
            const SizedBox(height: 16),
            _buildDoctorAdviceInput(),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: _saveProfile,
              child: const Text('保存档案'),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildAllergenSelection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '过敏原选择',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: _availableAllergens.map((allergen) {
                return FilterChip(
                  label: Text(allergen),
                  selected: _selectedAllergens.contains(allergen),
                  onSelected: (selected) {
                    setState(() {
                      if (selected) {
                        _selectedAllergens.add(allergen);
                      } else {
                        _selectedAllergens.remove(allergen);
                      }
                    });
                  },
                );
              }).toList(),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildSensitivitySlider() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '敏感度等级',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Slider(
              value: _sensitivityLevel.toDouble(),
              min: 1,
              max: 5,
              divisions: 4,
              label: _getSensitivityLabel(_sensitivityLevel),
              onChanged: (value) {
                setState(() {
                  _sensitivityLevel = value.round();
                });
              },
            ),
            Text(
              _getSensitivityDescription(_sensitivityLevel),
              style: TextStyle(fontSize: 12, color: Colors.grey[600]),
            ),
          ],
        ),
      ),
    );
  }
  
  String _getSensitivityLabel(int level) {
    switch (level) {
      case 1: return '很低';
      case 2: return '低';
      case 3: return '中等';
      case 4: return '高';
      case 5: return '很高';
      default: return '中等';
    }
  }
  
  String _getSensitivityDescription(int level) {
    switch (level) {
      case 1: return '对花粉不太敏感,很少出现过敏症状';
      case 2: return '轻微敏感,偶尔会有轻微症状';
      case 3: return '中等敏感,在花粉浓度较高时会有症状';
      case 4: return '比较敏感,经常出现过敏症状';
      case 5: return '非常敏感,即使花粉浓度不高也会有强烈反应';
      default: return '';
    }
  }
  
  void _saveProfile() {
    if (_formKey.currentState!.validate()) {
      final profile = AllergyProfile(
        userId: 'current_user',
        allergens: _selectedAllergens,
        sensitivityLevel: _sensitivityLevel,
        symptoms: _symptoms,
        medications: _medications,
        doctorAdvice: _doctorAdvice,
        lastUpdated: DateTime.now(),
      );
      
      // 保存到本地存储
      _saveToLocalStorage(profile);
      
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('过敏档案已保存')),
      );
      
      Navigator.pop(context);
    }
  }
  
  void _saveToLocalStorage(AllergyProfile profile) {
    // 使用SharedPreferences或数据库保存
  }
}

3. 智能提醒系统

定时提醒功能

class PollenNotificationService {
  static const String channelId = 'pollen_alerts';
  static const String channelName = '花粉提醒';
  
  // 初始化通知服务
  static Future<void> initialize() async {
    final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings();
    const initSettings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );
    
    await flutterLocalNotificationsPlugin.initialize(initSettings);
    
    // 创建通知渠道
    const androidChannel = AndroidNotificationChannel(
      channelId,
      channelName,
      description: '花粉浓度提醒通知',
      importance: Importance.high,
    );
    
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(androidChannel);
  }
  
  // 发送即时提醒
  static Future<void> sendImmediateAlert({
    required String title,
    required String body,
    required String cityName,
    required int pollenIndex,
  }) async {
    final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    
    const androidDetails = AndroidNotificationDetails(
      channelId,
      channelName,
      channelDescription: '花粉浓度提醒通知',
      importance: Importance.high,
      priority: Priority.high,
      icon: '@mipmap/ic_launcher',
    );
    
    const iosDetails = DarwinNotificationDetails();
    const details = NotificationDetails(android: androidDetails, iOS: iosDetails);
    
    await flutterLocalNotificationsPlugin.show(
      0,
      title,
      body,
      details,
      payload: json.encode({
        'type': 'pollen_alert',
        'city': cityName,
        'index': pollenIndex,
      }),
    );
  }
  
  // 设置定时提醒
  static Future<void> scheduleDaily({
    required String cityName,
    required TimeOfDay time,
    required AllergyProfile profile,
  }) async {
    final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    
    await flutterLocalNotificationsPlugin.zonedSchedule(
      1,
      '今日花粉提醒',
      '正在获取$cityName的花粉数据...',
      _nextInstanceOfTime(time),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          channelId,
          channelName,
          channelDescription: '每日花粉提醒',
        ),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time,
    );
  }
  
  static tz.TZDateTime _nextInstanceOfTime(TimeOfDay time) {
    final now = tz.TZDateTime.now(tz.local);
    tz.TZDateTime scheduledDate = tz.TZDateTime(
      tz.local,
      now.year,
      now.month,
      now.day,
      time.hour,
      time.minute,
    );
    
    if (scheduledDate.isBefore(now)) {
      scheduledDate = scheduledDate.add(const Duration(days: 1));
    }
    
    return scheduledDate;
  }
  
  // 高风险预警
  static Future<void> sendHighRiskAlert({
    required String cityName,
    required int pollenIndex,
    required String riskLevel,
    required List<String> tips,
  }) async {
    if (pollenIndex >= 80) {
      await sendImmediateAlert(
        title: '⚠️ 高花粉风险预警',
        body: '$cityName花粉指数达到$pollenIndex$riskLevel),请注意防护!',
        cityName: cityName,
        pollenIndex: pollenIndex,
      );
    }
  }
}

class NotificationSettingsPage extends StatefulWidget {
  
  State<NotificationSettingsPage> createState() => _NotificationSettingsPageState();
}

class _NotificationSettingsPageState extends State<NotificationSettingsPage> {
  bool _enableDailyReminder = true;
  bool _enableHighRiskAlert = true;
  bool _enableWeatherAlert = false;
  TimeOfDay _reminderTime = const TimeOfDay(hour: 8, minute: 0);
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('提醒设置')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    '提醒类型',
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 12),
                  SwitchListTile(
                    title: const Text('每日花粉提醒'),
                    subtitle: const Text('每天定时推送花粉浓度信息'),
                    value: _enableDailyReminder,
                    onChanged: (value) {
                      setState(() {
                        _enableDailyReminder = value;
                      });
                    },
                  ),
                  SwitchListTile(
                    title: const Text('高风险预警'),
                    subtitle: const Text('花粉浓度达到高风险时立即提醒'),
                    value: _enableHighRiskAlert,
                    onChanged: (value) {
                      setState(() {
                        _enableHighRiskAlert = value;
                      });
                    },
                  ),
                  SwitchListTile(
                    title: const Text('天气变化提醒'),
                    subtitle: const Text('天气变化可能影响花粉浓度时提醒'),
                    value: _enableWeatherAlert,
                    onChanged: (value) {
                      setState(() {
                        _enableWeatherAlert = value;
                      });
                    },
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          if (_enableDailyReminder) ...[
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      '提醒时间',
                      style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 12),
                    ListTile(
                      leading: const Icon(Icons.access_time),
                      title: const Text('每日提醒时间'),
                      subtitle: Text(_reminderTime.format(context)),
                      trailing: const Icon(Icons.chevron_right),
                      onTap: _selectTime,
                    ),
                  ],
                ),
              ),
            ),
          ],
          const SizedBox(height: 32),
          ElevatedButton(
            onPressed: _saveSettings,
            child: const Text('保存设置'),
          ),
        ],
      ),
    );
  }
  
  void _selectTime() async {
    final time = await showTimePicker(
      context: context,
      initialTime: _reminderTime,
    );
    
    if (time != null) {
      setState(() {
        _reminderTime = time;
      });
    }
  }
  
  void _saveSettings() {
    // 保存设置到本地存储
    // 设置定时提醒
    if (_enableDailyReminder) {
      PollenNotificationService.scheduleDaily(
        cityName: '北京市', // 从用户设置获取
        time: _reminderTime,
        profile: AllergyProfile(/* 用户档案 */),
      );
    }
    
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('设置已保存')),
    );
  }
}

4. 地图可视化

花粉浓度地图

class PollenMapPage extends StatefulWidget {
  
  State<PollenMapPage> createState() => _PollenMapPageState();
}

class _PollenMapPageState extends State<PollenMapPage> {
  GoogleMapController? _mapController;
  Set<Marker> _markers = {};
  Set<Circle> _circles = {};
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('花粉浓度地图')),
      body: GoogleMap(
        initialCameraPosition: const CameraPosition(
          target: LatLng(39.9042, 116.4074), // 北京
          zoom: 5,
        ),
        markers: _markers,
        circles: _circles,
        onMapCreated: _onMapCreated,
      ),
    );
  }
  
  void _onMapCreated(GoogleMapController controller) {
    _mapController = controller;
    _loadPollenMarkers();
  }
  
  void _loadPollenMarkers() {
    // 加载各城市花粉数据标记
    for (var pollenData in _allPollenData) {
      _markers.add(Marker(
        markerId: MarkerId(pollenData.id),
        position: _getCityLatLng(pollenData.cityName),
        icon: _getMarkerIcon(pollenData.overallIndex),
        infoWindow: InfoWindow(
          title: pollenData.cityName,
          snippet: '花粉指数: ${pollenData.overallIndex}',
        ),
      ));
      
      _circles.add(Circle(
        circleId: CircleId(pollenData.id),
        center: _getCityLatLng(pollenData.cityName),
        radius: pollenData.overallIndex * 1000.0,
        fillColor: pollenData.overallColor.withValues(alpha: 0.3),
        strokeColor: pollenData.overallColor,
        strokeWidth: 2,
      ));
    }
    setState(() {});
  }
}

5. 历史数据分析

趋势图表展示

class HistoryAnalysisPage extends StatefulWidget {
  
  State<HistoryAnalysisPage> createState() => _HistoryAnalysisPageState();
}

class _HistoryAnalysisPageState extends State<HistoryAnalysisPage> {
  List<FlSpot> _pollenTrendData = [];
  String _selectedPeriod = '7天';
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('历史趋势')),
      body: Column(
        children: [
          _buildPeriodSelector(),
          Expanded(child: _buildTrendChart()),
          _buildStatistics(),
        ],
      ),
    );
  }
  
  Widget _buildTrendChart() {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: LineChart(
          LineChartData(
            gridData: FlGridData(show: true),
            titlesData: FlTitlesData(show: true),
            borderData: FlBorderData(show: true),
            lineBarsData: [
              LineChartBarData(
                spots: _pollenTrendData,
                isCurved: true,
                color: Colors.green,
                barWidth: 3,
                dotData: FlDotData(show: true),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

6. 社区功能

用户分享和交流

class CommunityPage extends StatefulWidget {
  
  State<CommunityPage> createState() => _CommunityPageState();
}

class _CommunityPageState extends State<CommunityPage> {
  List<CommunityPost> _posts = [];
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('过敏社区'),
        actions: [
          IconButton(
            onPressed: _createPost,
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) {
          return _buildPostCard(_posts[index]);
        },
      ),
    );
  }
  
  Widget _buildPostCard(CommunityPost post) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                CircleAvatar(child: Text(post.authorName[0])),
                const SizedBox(width: 8),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(post.authorName, style: const TextStyle(fontWeight: FontWeight.bold)),
                      Text(post.location, style: const TextStyle(fontSize: 12, color: Colors.grey)),
                    ],
                  ),
                ),
                Text(post.timeAgo, style: const TextStyle(fontSize: 12, color: Colors.grey)),
              ],
            ),
            const SizedBox(height: 12),
            Text(post.content),
            if (post.pollenIndex != null) ...[
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.blue.withValues(alpha: 0.1),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text('当地花粉指数: ${post.pollenIndex}'),
              ),
            ],
            const SizedBox(height: 12),
            Row(
              children: [
                TextButton.icon(
                  onPressed: () => _likePost(post),
                  icon: Icon(post.isLiked ? Icons.thumb_up : Icons.thumb_up_outlined),
                  label: Text('${post.likeCount}'),
                ),
                TextButton.icon(
                  onPressed: () => _commentPost(post),
                  icon: const Icon(Icons.comment_outlined),
                  label: Text('${post.commentCount}'),
                ),
                TextButton.icon(
                  onPressed: () => _sharePost(post),
                  icon: const Icon(Icons.share_outlined),
                  label: const Text('分享'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class CommunityPost {
  final String id;
  final String authorName;
  final String location;
  final String content;
  final int? pollenIndex;
  final DateTime createTime;
  final int likeCount;
  final int commentCount;
  final bool isLiked;
  
  CommunityPost({
    required this.id,
    required this.authorName,
    required this.location,
    required this.content,
    this.pollenIndex,
    required this.createTime,
    required this.likeCount,
    required this.commentCount,
    required this.isLiked,
  });
  
  String get timeAgo {
    final now = DateTime.now();
    final diff = now.difference(createTime);
    if (diff.inMinutes < 1) return '刚刚';
    if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
    if (diff.inHours < 24) return '${diff.inHours}小时前';
    return '${diff.inDays}天前';
  }
}

7. 健康数据记录

症状日记功能

class SymptomDiary {
  final String id;
  final DateTime date;
  final List<String> symptoms;
  final int severityLevel;        // 严重程度 1-5
  final String medication;        // 使用的药物
  final String notes;            // 备注
  final int pollenIndex;         // 当日花粉指数
  final String weather;          // 天气状况
  
  SymptomDiary({
    required this.id,
    required this.date,
    required this.symptoms,
    required this.severityLevel,
    required this.medication,
    required this.notes,
    required this.pollenIndex,
    required this.weather,
  });
}

class SymptomDiaryPage extends StatefulWidget {
  
  State<SymptomDiaryPage> createState() => _SymptomDiaryPageState();
}

class _SymptomDiaryPageState extends State<SymptomDiaryPage> {
  List<SymptomDiary> _diaries = [];
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('症状日记'),
        actions: [
          IconButton(
            onPressed: _addDiary,
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: _diaries.length,
        itemBuilder: (context, index) {
          return _buildDiaryCard(_diaries[index]);
        },
      ),
    );
  }
  
  Widget _buildDiaryCard(SymptomDiary diary) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  DateFormat('yyyy年MM月dd日').format(diary.date),
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: _getSeverityColor(diary.severityLevel).withValues(alpha: 0.2),
                    borderRadius: BorderRadius.circular(4),
                  ),
                  child: Text(
                    _getSeverityText(diary.severityLevel),
                    style: TextStyle(
                      fontSize: 12,
                      color: _getSeverityColor(diary.severityLevel),
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 4,
              children: diary.symptoms.map((symptom) {
                return Chip(
                  label: Text(symptom),
                  backgroundColor: Colors.red.withValues(alpha: 0.1),
                  labelStyle: const TextStyle(fontSize: 10),
                );
              }).toList(),
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Icon(Icons.eco, size: 16, color: Colors.green),
                const SizedBox(width: 4),
                Text('花粉指数: ${diary.pollenIndex}', style: const TextStyle(fontSize: 12)),
                const SizedBox(width: 16),
                Icon(Icons.wb_sunny, size: 16, color: Colors.orange),
                const SizedBox(width: 4),
                Text(diary.weather, style: const TextStyle(fontSize: 12)),
              ],
            ),
            if (diary.medication.isNotEmpty) ...[
              const SizedBox(height: 8),
              Row(
                children: [
                  const Icon(Icons.medication, size: 16, color: Colors.blue),
                  const SizedBox(width: 4),
                  Text('用药: ${diary.medication}', style: const TextStyle(fontSize: 12)),
                ],
              ),
            ],
            if (diary.notes.isNotEmpty) ...[
              const SizedBox(height: 8),
              Text(diary.notes, style: const TextStyle(fontSize: 12, color: Colors.grey)),
            ],
          ],
        ),
      ),
    );
  }
  
  Color _getSeverityColor(int level) {
    switch (level) {
      case 1: return Colors.green;
      case 2: return Colors.lightGreen;
      case 3: return Colors.orange;
      case 4: return Colors.deepOrange;
      case 5: return Colors.red;
      default: return Colors.grey;
    }
  }
  
  String _getSeverityText(int level) {
    switch (level) {
      case 1: return '轻微';
      case 2: return '较轻';
      case 3: return '中等';
      case 4: return '较重';
      case 5: return '严重';
      default: return '未知';
    }
  }
}

8. 医疗资源整合

医院和医生推荐

class MedicalResource {
  final String id;
  final String name;
  final String type;             // 医院、诊所、药店
  final String address;
  final String phone;
  final double distance;         // 距离(公里)
  final double rating;
  final List<String> specialties; // 专科
  final String openTime;
  final bool hasEmergency;       // 是否有急诊
  
  MedicalResource({
    required this.id,
    required this.name,
    required this.type,
    required this.address,
    required this.phone,
    required this.distance,
    required this.rating,
    required this.specialties,
    required this.openTime,
    required this.hasEmergency,
  });
}

class MedicalResourcePage extends StatefulWidget {
  
  State<MedicalResourcePage> createState() => _MedicalResourcePageState();
}

class _MedicalResourcePageState extends State<MedicalResourcePage> {
  List<MedicalResource> _resources = [];
  String _selectedType = '全部';
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('医疗资源')),
      body: Column(
        children: [
          _buildTypeFilter(),
          Expanded(child: _buildResourceList()),
        ],
      ),
    );
  }
  
  Widget _buildResourceCard(MedicalResource resource) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: InkWell(
        onTap: () => _showResourceDetail(resource),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Icon(_getResourceIcon(resource.type), color: Colors.blue),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      resource.name,
                      style: const TextStyle(fontWeight: FontWeight.bold),
                    ),
                  ),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                    decoration: BoxDecoration(
                      color: Colors.blue.withValues(alpha: 0.1),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      resource.type,
                      style: const TextStyle(fontSize: 10, color: Colors.blue),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Icon(Icons.location_on, size: 14, color: Colors.grey),
                  const SizedBox(width: 4),
                  Expanded(
                    child: Text(
                      resource.address,
                      style: const TextStyle(fontSize: 12, color: Colors.grey),
                    ),
                  ),
                  Text(
                    '${resource.distance.toStringAsFixed(1)}km',
                    style: const TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                ],
              ),
              const SizedBox(height: 4),
              Row(
                children: [
                  Icon(Icons.star, size: 14, color: Colors.amber),
                  const SizedBox(width: 4),
                  Text(
                    resource.rating.toStringAsFixed(1),
                    style: const TextStyle(fontSize: 12),
                  ),
                  const SizedBox(width: 16),
                  Icon(Icons.access_time, size: 14, color: Colors.grey),
                  const SizedBox(width: 4),
                  Text(
                    resource.openTime,
                    style: const TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                ],
              ),
              if (resource.specialties.isNotEmpty) ...[
                const SizedBox(height: 8),
                Wrap(
                  spacing: 4,
                  children: resource.specialties.take(3).map((specialty) {
                    return Chip(
                      label: Text(specialty),
                      backgroundColor: Colors.green.withValues(alpha: 0.1),
                      labelStyle: const TextStyle(fontSize: 10),
                    );
                  }).toList(),
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
  
  IconData _getResourceIcon(String type) {
    switch (type) {
      case '医院': return Icons.local_hospital;
      case '诊所': return Icons.medical_services;
      case '药店': return Icons.local_pharmacy;
      default: return Icons.healing;
    }
  }
}

常见问题解答

1. 如何获取真实的花粉数据?

问题:应用中使用的是模拟数据,如何接入真实的花粉监测数据?

解答

  1. 气象部门API:联系当地气象部门获取花粉监测数据接口
  2. 环保部门数据:环保部门通常有空气质量和花粉监测数据
  3. 第三方服务:使用如AccuWeather、Weather Underground等服务
  4. 科研机构合作:与大学或研究机构合作获取数据
  5. 众包数据:建立用户上报机制,收集实地数据

实现示例

class RealPollenService {
  static Future<PollenData> getOfficialData(String cityCode) async {
    final response = await http.get(
      Uri.parse('https://api.weather.gov.cn/pollen/$cityCode'),
      headers: {'Authorization': 'Bearer YOUR_API_KEY'},
    );
    
    if (response.statusCode == 200) {
      return PollenData.fromOfficialJson(json.decode(response.body));
    }
    throw Exception('Failed to load official data');
  }
}

2. 如何提高花粉预测的准确性?

问题:如何让花粉浓度预测更加准确?

解答

  1. 多数据源融合:结合气象、植被、历史数据
  2. 机器学习模型:使用AI算法提高预测精度
  3. 实时校正:根据实测数据动态调整预测
  4. 地理因素考虑:考虑地形、植被分布等因素
  5. 季节性分析:分析不同季节的花粉规律

预测模型示例

class PollenPredictionModel {
  static Future<List<DailyForecast>> predictPollen({
    required String cityCode,
    required List<WeatherForecast> weatherForecast,
    required List<HistoricalData> historicalData,
  }) async {
    // 使用机器学习模型预测
    final predictions = <DailyForecast>[];
    
    for (int i = 0; i < 7; i++) {
      final weather = weatherForecast[i];
      final prediction = await _mlPredict(
        temperature: weather.temperature,
        humidity: weather.humidity,
        windSpeed: weather.windSpeed,
        precipitation: weather.precipitation,
        historicalAverage: _getHistoricalAverage(historicalData, i),
      );
      
      predictions.add(DailyForecast(
        date: DateTime.now().add(Duration(days: i)),
        pollenIndex: prediction.pollenIndex,
        weather: weather.condition,
        maxTemp: weather.maxTemp,
        minTemp: weather.minTemp,
      ));
    }
    
    return predictions;
  }
}

3. 如何实现个性化的过敏提醒?

问题:如何根据用户的过敏情况提供个性化提醒?

解答

  1. 过敏档案建立:收集用户过敏原、敏感度等信息
  2. 症状记录分析:分析用户历史症状与花粉的关联
  3. 个人阈值设定:为每个用户设定个性化的预警阈值
  4. 智能学习:根据用户反馈不断优化提醒策略
  5. 多维度考虑:结合天气、地理位置、时间等因素

4. 如何处理离线使用场景?

问题:用户在没有网络的情况下如何使用应用?

解答

  1. 本地数据缓存:缓存最近的花粉数据和预报
  2. 离线地图:下载离线地图数据
  3. 历史数据分析:基于历史数据提供趋势分析
  4. 本地计算:使用本地算法进行简单预测
  5. 数据同步:有网络时自动同步最新数据

5. 如何保护用户隐私?

问题:收集用户健康数据时如何保护隐私?

解答

  1. 数据加密:所有敏感数据进行加密存储
  2. 本地存储优先:尽量在本地处理数据
  3. 匿名化处理:上传数据时去除个人标识
  4. 用户授权:明确告知数据用途,获得用户同意
  5. 数据最小化:只收集必要的数据

隐私保护实现

class PrivacyManager {
  // 加密存储敏感数据
  static Future<void> saveEncryptedData(String key, String data) async {
    final encrypted = await _encrypt(data);
    await SharedPreferences.getInstance().then((prefs) {
      prefs.setString(key, encrypted);
    });
  }
  
  // 匿名化用户数据
  static Map<String, dynamic> anonymizeUserData(AllergyProfile profile) {
    return {
      'sensitivity_level': profile.sensitivityLevel,
      'allergen_count': profile.allergens.length,
      'symptom_count': profile.symptoms.length,
      'age_group': _getAgeGroup(profile.birthDate),
      'region': _getRegion(profile.location),
      // 不包含具体的个人信息
    };
  }
}

项目总结

核心功能回顾

本项目成功实现了一个功能完整的实时花粉浓度查询应用,主要功能包括:

花粉浓度查询应用

实时数据

预报功能

过敏提醒

城市选择

30个城市

4类花粉

5级指数

天气关联

7天预报

趋势分析

智能预测

风险评估

防护建议

健康指导

紧急处理

网格选择

实时切换

数据刷新

技术架构总览

用户界面层

业务逻辑层

数据访问层

NavigationBar导航

圆形指数展示

进度条可视化

预报列表

提醒卡片

花粉数据管理

风险等级计算

城市切换逻辑

时间格式化

模拟数据生成

计算属性缓存

状态管理

数据流程图

数据层 业务逻辑 界面层 用户 数据层 业务逻辑 界面层 用户 启动应用 初始化数据 生成花粉数据 返回30城市数据 加载北京数据 显示花粉指数 选择城市 切换城市 查找城市数据 返回城市数据 显示新城市数据 切换到预报页 获取预报数据 返回7天预报 显示预报列表

项目特色

  1. 健康导向设计:专注于过敏人群的实际需求
  2. 直观数据展示:圆形指数、进度条、颜色映射
  3. 智能风险评估:多维度分析过敏风险
  4. 专业防护建议:基于医学知识的建议系统
  5. 紧急处理指导:关键时刻的急救指南

学习收获

通过本项目的开发,可以掌握以下技能:

Flutter核心技能

  • 复杂状态管理
  • 自定义UI组件
  • 数据可视化技术
  • 异步数据处理

健康应用开发

  • 医疗数据处理
  • 风险评估算法
  • 用户体验优化
  • 隐私保护措施

UI设计技能

  • 圆形进度指示器
  • 动态颜色映射
  • 卡片式布局
  • 响应式设计

性能优化建议

  1. 数据缓存优化
class PollenDataCache {
  static final Map<String, CachedPollenData> _cache = {};
  
  static PollenData? getCachedData(String cityCode) {
    final cached = _cache[cityCode];
    if (cached != null && !cached.isExpired) {
      return cached.data;
    }
    return null;
  }
}
  1. 图表性能优化
// 使用fl_chart进行数据可视化
LineChart(
  LineChartData(
    lineBarsData: [
      LineChartBarData(
        spots: _optimizedDataPoints, // 数据点优化
        isCurved: true,
        dotData: FlDotData(show: false), // 隐藏点以提升性能
      ),
    ],
  ),
)

未来优化方向

  1. AI智能预测:集成机器学习模型提高预测准确性
  2. AR实景显示:使用AR技术显示实时花粉浓度
  3. 社交功能:用户分享和社区交流
  4. 医疗整合:对接医院和医生资源
  5. 可穿戴设备:支持智能手表等设备

部署和发布

  1. 多平台适配
# Android发布
flutter build apk --release
flutter build appbundle --release

# iOS发布
flutter build ios --release

# Web发布
flutter build web --release
  1. 应用商店优化
  • 关键词:花粉、过敏、健康、天气
  • 应用描述:突出健康价值和实用性
  • 截图展示:核心功能界面
  • 用户评价:收集真实用户反馈

本项目展示了Flutter在健康类应用开发中的强大能力,通过科学的数据分析和人性化的界面设计,为过敏人群提供了实用的健康管理工具。项目代码结构清晰,功能完整,适合作为Flutter健康应用开发的参考案例。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐