过期提醒是药箱管理应用的核心功能之一。药品过期后不仅失去疗效,还可能产生有害物质。通过过期提醒功能,用户可以及时发现并处理过期或即将过期的药品。

功能设计思路

过期提醒页面分为两个Tab:已过期即将过期。已过期Tab显示所有过期药品,即将过期Tab显示30天内将要过期的药品。使用不同颜色区分两种状态,让用户能够快速识别紧急程度。

Tab页面结构

使用DefaultTabController实现Tab切换:


Widget build(BuildContext context) {
  return DefaultTabController(
    length: 2,
    child: Scaffold(
      appBar: AppBar(
        title: const Text('过期提醒'),
        bottom: const TabBar(
          tabs: [
            Tab(text: '已过期'),
            Tab(text: '即将过期'),
          ],
        ),
      ),
      body: Consumer<MedicineProvider>(
        builder: (context, provider, child) {
          return TabBarView(
            children: [
              _buildMedicineList(provider.expiredMedicines, true),
              _buildMedicineList(provider.expiringSoonMedicines, false),
            ],
          );
        },
      ),
    ),
  );
}

DefaultTabController管理Tab状态,length: 2表示有两个Tab。AppBar的bottom属性放置TabBar,定义两个Tab标签。TabBarView包含两个子页面,分别显示已过期和即将过期的药品。使用Consumer监听Provider,当药品数据变化时自动更新。

药品列表构建

根据是否过期显示不同的空状态:

Widget _buildMedicineList(List<Medicine> medicines, bool isExpired) {
  if (medicines.isEmpty) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            isExpired ? Icons.check_circle : Icons.access_time,
            size: 64.sp,
            color: Colors.grey[300],
          ),
          SizedBox(height: 16.h),
          Text(
            isExpired ? '没有过期药品' : '没有即将过期的药品',
            style: TextStyle(color: Colors.grey[500], fontSize: 16.sp),
          ),
        ],
      ),
    );
  }

  return ListView.builder(
    padding: EdgeInsets.all(16.w),
    itemCount: medicines.length,
    itemBuilder: (context, index) {
      final medicine = medicines[index];
      return _buildMedicineCard(medicine, isExpired);
    },
  );
}

列表为空时显示友好的空状态提示。已过期Tab显示对勾图标和"没有过期药品",即将过期Tab显示时钟图标和"没有即将过期的药品"。有数据时使用ListView.builder构建列表。

药品卡片设计

每个药品卡片显示完整的过期信息:

Widget _buildMedicineCard(Medicine medicine, bool isExpired) {
  final color = isExpired ? Colors.red : Colors.orange;
  final statusText = isExpired
      ? '已过期 ${-medicine.daysUntilExpiry} 天'
      : '${medicine.daysUntilExpiry} 天后过期';

  return GestureDetector(
    onTap: () => Get.to(() => MedicineDetailScreen(medicine: medicine)),
    child: Container(
      margin: EdgeInsets.only(bottom: 12.h),
      padding: EdgeInsets.all(12.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        border: Border(left: BorderSide(color: color, width: 4.w)),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Row(
        children: [
          Container(
            width: 48.w,
            height: 48.w,
            decoration: BoxDecoration(
              color: color.withOpacity(0.1),
              borderRadius: BorderRadius.circular(8.r),
            ),
            child: Icon(
              isExpired ? Icons.warning : Icons.access_time,
              color: color,
            ),
          ),
          SizedBox(width: 12.w),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  medicine.name,
                  style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 4.h),
                Text(
                  '有效期至: ${DateFormat('yyyy-MM-dd').format(medicine.expiryDate)}',
                  style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]),
                ),
              ],
            ),
          ),
          Container(
            padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
            decoration: BoxDecoration(
              color: color.withOpacity(0.1),
              borderRadius: BorderRadius.circular(10.r),
            ),
            child: Text(
              statusText,
              style: TextStyle(fontSize: 10.sp, color: color),
            ),
          ),
        ],
      ),
    ),
  );
}

卡片左侧有一条彩色边框,已过期使用红色,即将过期使用橙色。这种设计让用户能够快速识别药品状态。左侧图标也使用对应颜色,已过期显示警告图标,即将过期显示时钟图标。

状态文字计算:已过期药品显示"已过期X天",使用负数的剩余天数。即将过期药品显示"X天后过期",使用正数的剩余天数。这种表述方式直观明了,用户一眼就能看出紧急程度。

信息展示:中间区域显示药品名称和有效期日期。名称使用加粗字体,是卡片中最重要的信息。有效期使用灰色小字体,作为补充说明。右侧的状态标签使用圆角矩形,背景色和文字颜色都使用状态颜色。

Provider过期逻辑

Provider中实现过期判断:

List<Medicine> get expiredMedicines =>
    _medicines.where((m) => m.isExpired).toList();

List<Medicine> get expiringSoonMedicines =>
    _medicines.where((m) => m.isExpiringSoon).toList();

expiredMedicines筛选所有已过期的药品,expiringSoonMedicines筛选即将过期的药品。这两个计算属性每次访问时都会重新筛选,确保数据始终是最新的。

Medicine模型判断

Medicine模型中定义过期判断逻辑:

bool get isExpired => DateTime.now().isAfter(expiryDate);

bool get isExpiringSoon {
  final daysUntilExpiry = expiryDate.difference(DateTime.now()).inDays;
  return daysUntilExpiry <= 30 && daysUntilExpiry > 0;
}

int get daysUntilExpiry => expiryDate.difference(DateTime.now()).inDays;

isExpired判断当前时间是否晚于有效期。isExpiringSoon判断剩余天数是否在0到30天之间。daysUntilExpiry计算剩余天数,正数表示未过期,负数表示已过期。

颜色语义化

使用颜色传达状态信息是一种有效的设计手法。红色代表危险和警告,用于已过期药品。橙色代表注意和提醒,用于即将过期药品。这种颜色编码符合用户的认知习惯,无需文字说明就能理解状态的严重程度。

左侧边框设计

卡片左侧的彩色边框是一个巧妙的设计。它不会占用太多空间,但能够有效地区分不同状态。用户浏览列表时,通过边框颜色就能快速识别哪些药品需要优先处理。这种设计在很多应用中都有使用,是一种成熟的UI模式。

日期格式化

使用intl包的DateFormat格式化日期:

'有效期至: ${DateFormat('yyyy-MM-dd').format(medicine.expiryDate)}'

DateFormat提供了灵活的日期格式化功能。'yyyy-MM-dd’格式输出"年-月-日",这是中国用户习惯的日期格式。格式化后的日期清晰易读,避免了时间戳等不友好的显示方式。

过期天数计算

计算药品距离过期的天数:

int get daysUntilExpiry {
  final now = DateTime.now();
  final today = DateTime(now.year, now.month, now.day);
  final expiry = DateTime(expiryDate.year, expiryDate.month, expiryDate.day);
  return expiry.difference(today).inDays;
}

String get expiryStatusText {
  final days = daysUntilExpiry;
  if (days < 0) {
    return '已过期 ${-days} 天';
  } else if (days == 0) {
    return '今天过期';
  } else if (days <= 7) {
    return '$days 天后过期';
  } else if (days <= 30) {
    return '$days 天后过期';
  } else {
    return '${(days / 30).floor()} 个月后过期';
  }
}

计算时先将日期标准化到当天0点,避免时间部分影响计算结果。根据剩余天数返回不同的文字描述,让用户更直观地了解过期状态。超过30天的显示月数,避免数字过大。

批量操作功能

支持批量删除过期药品:

Widget _buildBatchActions(List<Medicine> medicines) {
  return Container(
    padding: EdgeInsets.all(16.w),
    child: Row(
      children: [
        Expanded(
          child: ElevatedButton.icon(
            onPressed: () => _showBatchDeleteDialog(medicines),
            icon: const Icon(Icons.delete_sweep),
            label: Text('批量删除 (${medicines.length})'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
              foregroundColor: Colors.white,
              padding: EdgeInsets.symmetric(vertical: 12.h),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
        ),
      ],
    ),
  );
}

void _showBatchDeleteDialog(List<Medicine> medicines) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('批量删除'),
      content: Text('确定要删除这 ${medicines.length} 种过期药品吗?此操作不可撤销。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            for (var medicine in medicines) {
              context.read<MedicineProvider>().deleteMedicine(medicine.id);
            }
            Navigator.pop(context);
            Get.snackbar('成功', '已删除 ${medicines.length} 种过期药品');
          },
          child: const Text('删除', style: TextStyle(color: Colors.red)),
        ),
      ],
    ),
  );
}

批量删除功能让用户可以一次性清理所有过期药品,避免逐个删除的繁琐操作。删除前显示确认对话框,防止误操作。删除后显示成功提示,让用户知道操作已完成。

过期提醒通知

设置过期提醒通知:

void _scheduleExpiryNotifications() {
  final medicines = context.read<MedicineProvider>().expiringSoonMedicines;
  
  for (var medicine in medicines) {
    final daysUntil = medicine.daysUntilExpiry;
    
    if (daysUntil == 7) {
      _showNotification(
        '药品即将过期',
        '${medicine.name} 将在7天后过期,请及时处理',
      );
    } else if (daysUntil == 3) {
      _showNotification(
        '药品即将过期',
        '${medicine.name} 将在3天后过期,请尽快处理',
      );
    } else if (daysUntil == 1) {
      _showNotification(
        '药品明天过期',
        '${medicine.name} 明天就要过期了,请立即处理',
      );
    }
  }
}

在药品过期前7天、3天、1天分别发送提醒通知。通知内容包含药品名称和剩余天数,让用户明确知道需要处理哪些药品。这种渐进式提醒确保用户不会错过重要信息。

过期统计信息

显示过期药品的统计信息:

Widget _buildExpiryStats(List<Medicine> expired, List<Medicine> expiringSoon) {
  return Container(
    margin: EdgeInsets.all(16.w),
    padding: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withOpacity(0.05),
          blurRadius: 10,
          offset: const Offset(0, 2),
        ),
      ],
    ),
    child: Row(
      children: [
        Expanded(
          child: _buildStatItem(
            '已过期',
            expired.length,
            Colors.red,
            Icons.warning,
          ),
        ),
        Container(width: 1, height: 50.h, color: Colors.grey[300]),
        Expanded(
          child: _buildStatItem(
            '即将过期',
            expiringSoon.length,
            Colors.orange,
            Icons.access_time,
          ),
        ),
      ],
    ),
  );
}

Widget _buildStatItem(String label, int count, Color color, IconData icon) {
  return Column(
    children: [
      Icon(icon, color: color, size: 24.sp),
      SizedBox(height: 8.h),
      Text(
        '$count',
        style: TextStyle(
          fontSize: 24.sp,
          fontWeight: FontWeight.bold,
          color: color,
        ),
      ),
      Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey[600])),
    ],
  );
}

统计卡片显示已过期和即将过期的药品数量,使用不同颜色区分。这让用户能够快速了解整体情况,决定优先处理哪些药品。

性能优化

过期提醒功能的性能优化包括:

  1. 计算属性缓存:过期判断使用计算属性,避免重复计算
  2. 列表排序:按过期紧急程度排序,最紧急的排在前面
  3. 懒加载:ListView.builder只构建可见的卡片
  4. 条件渲染:空状态和有数据状态使用条件渲染

用户体验设计

过期提醒的用户体验设计包括:

  1. 颜色编码:红色表示已过期,橙色表示即将过期
  2. 左侧边框:快速识别药品状态
  3. 状态标签:显示具体的过期天数
  4. 批量操作:支持一次性删除多个过期药品
  5. 渐进提醒:在过期前多次提醒用户

总结

过期提醒功能通过Tab分页和颜色编码,让用户能够快速识别和处理过期药品。左侧边框和状态标签提供了清晰的视觉提示,日期格式化让信息易于理解。Provider管理过期判断逻辑,保持代码的清晰和可维护性。


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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐