请添加图片描述

前言

用户打开备忘录列表,看到 30 条记录,哪些是刚刚创建的?如果没有任何视觉区分,新老数据混在一起,用户容易迷失。

一个简洁而有效的方案是:24 小时内创建的条目右侧渲染一个 “NEW” 角标。这个微交互不占额外空间,不需要用户做任何操作,信息传达却非常清晰——“这条是新的,你刚创建不久”。

本文将拆解这个"24 小时新建标签"的完整实现,包括时间差计算、视觉设计和条件渲染逻辑。

项目仓库:todo_flutter_harmony

需求细化

  1. 时间窗口:从当前时间往前推 24 小时,在这个窗口内的条目标记为"新"
  2. 视觉样式:一个圆角小标签,显示 “NEW” 文字,颜色醒目但不喧宾夺主
  3. 自动过期:24 小时后自动消失,无需手动清除

核心逻辑:时间差计算

Dart 的 DateTime 提供了直观的 difference 方法:

bool isNew(DateTime createdAt) {
  final now = DateTime.now();
  final difference = now.difference(createdAt);
  return difference.inHours < 24;
}

difference 返回一个 Duration 对象,提供了多个单位的访问器:

final duration = DateTime.now().difference(createdAt);

duration.inDays;      // 天数差
duration.inHours;     // 小时差(绝对值,忽略分钟)
duration.inMinutes;   // 分钟差
duration.inSeconds;   // 秒数差

注意:inHours 返回的是整数小时(向下取整),所以 difference.inHours < 24 意味着从创建时间到现在的完整小时数小于 24,即创建时间不超过 24 小时。

精确时间差判断

如果需求改为「在创建后的未来 24 小时内显示,之后自动消失」,还需要处理一个边界情况:创建时间在未来(比如设备时间被篡改)。更健壮的写法:

bool isNew(DateTime createdAt) {
  final now = DateTime.now();

  // 防御:如果创建时间在未来,不算"新"(可能是异常数据)
  if (createdAt.isAfter(now)) return false;

  // 24 小时内的才是"新"
  return now.difference(createdAt).inHours < 24;
}

视觉组件:NewBadge

class NewBadge extends StatelessWidget {
  const NewBadge({super.key});

  
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
      decoration: BoxDecoration(
        color: const Color(0xFF4DB6AC),
        borderRadius: BorderRadius.circular(4),
      ),
      child: const Text(
        'NEW',
        style: TextStyle(
          color: Colors.white,
          fontSize: 10,
          fontWeight: FontWeight.w800,
          letterSpacing: 0.5,
        ),
      ),
    );
  }
}

设计考量:

  1. fontSize: 10:小而精,不抢占标题的视觉权重
  2. fontWeight: w800:超粗体,在小字号下保持可读性
  3. letterSpacing: 0.5:微字间距,让 “NEW” 三个字母不挤在一起
  4. borderRadius: 4:小圆角,呼应 Material 3 设计语言
  5. 颜色 #4DB6AC:与主主题的种子色一致

在备忘录卡片中使用

最典型的使用场景是在 MemoCard 的标题右侧:

class MemoCard extends StatelessWidget {
  final Memo memo;

  const MemoCard({super.key, required this.memo});

  
  Widget build(BuildContext context) {
    return Card(
      elevation: 1,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(14),
        child: Row(
          children: [
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // 标题行:标题 + 可选 NEW 标签
                  Row(
                    children: [
                      Flexible(
                        child: Text(
                          memo.title,
                          style: const TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.w600,
                          ),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      if (_isNew(memo.createdAt)) ...[
                        const SizedBox(width: 6),
                        const NewBadge(),
                      ],
                    ],
                  ),
                  const SizedBox(height: 6),
                  // 内容预览
                  Text(
                    memo.content,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey.shade600,
                      height: 1.4,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  bool _isNew(DateTime createdAt) {
    final now = DateTime.now();
    if (createdAt.isAfter(now)) return false;
    return now.difference(createdAt).inHours < 24;
  }
}

优化:避免在 build 中调用 DateTime.now()

直接在 build 方法中调用 DateTime.now() 有一个问题:widget 每次重建都会重新计算,但这个结果只有到下一小时才可能变化。

对于简单的场景这不是性能瓶颈。但如果列表很长(100+ 条),可以在 Provider 层面做一次判断,然后把结果缓存在内存中:

class MemoProvider extends ChangeNotifier {
  List<Memo> _memos = [];

  List<Memo> get memos => _memos;

  // 缓存 NEW 标记结果,避免在 build 中重复计算
  final Map<int, bool> _newCache = {};

  bool isNew(Memo memo) {
    return _newCache.putIfAbsent(memo.id!, () {
      final diff = DateTime.now().difference(memo.createdAt);
      return diff.inHours < 24;
    });
  }

  void loadMemos() async {
    _memos = await DatabaseHelper.instance.getAllMemos();
    _newCache.clear();  // 重新加载时清空缓存
    notifyListeners();
  }
}

然后在 MemoCard 中用 context.watch<MemoProvider>().isNew(memo) 替代本地计算。这样每当 notifyListeners() 触发时,DateTime.now() 只会在 Provider 层执行一次。

进阶:相对时间显示

“NEW” 标签适合 24 小时内的新条目。过了 24 小时后,可以显示相对时间,让用户知道这条备忘录是"多久之前"创建的:

String _formatRelativeTime(DateTime createdAt) {
  final diff = DateTime.now().difference(createdAt);

  if (diff.inMinutes < 1) return '刚刚';
  if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
  if (diff.inHours < 24) return '${diff.inHours}小时前';
  if (diff.inDays < 7) return '${diff.inDays}天前';
  if (diff.inDays < 30) return '${(diff.inDays / 7).floor()}周前';
  return DateFormat('yyyy-MM-dd').format(createdAt);
}

结合显示逻辑:

Widget _buildTimeAgo(DateTime createdAt) {
  final isNew = DateTime.now().difference(createdAt).inHours < 24;
  final relativeTime = _formatRelativeTime(createdAt);

  return Row(
    children: [
      if (isNew) const NewBadge(),
      if (isNew) const SizedBox(width: 6),
      Text(
        relativeTime,
        style: TextStyle(
          fontSize: 12,
          color: Colors.grey.shade400,
        ),
      ),
    ],
  );
}

效果:

  • 5 分钟前创建的:[NEW] 5分钟前
  • 昨天创建的:1天前(NEW 标签已消失)
  • 一周前创建的:1周前

鸿蒙兼容性

整个组件仅依赖 DateTime.now()Duration 这些 Dart 核心库的类型,与平台完全无关。Container + BorderRadius 的样式渲染在 Flutter 渲染管线中统一处理。

唯一需要注意的是设备时间准确性——如果用户设备时间不准确(比如手动调到了未来),“NEW” 标签可能永远不会显示或永远不会消失。但实际上这是所有平台共有的问题,不算是鸿蒙特有问题。

总结

24 小时 NEW 标签的实现可以用五行代码概括核心逻辑:

if (DateTime.now().difference(item.createdAt).inHours < 24) {
  return const NewBadge();
}

加上视觉设计(Container + Text 的组合),总共不到 30 行代码。它虽小,却是用户体验中"润物细无声"的那一类——用户可能不会主动注意到它的存在,但它让新数据一目了然,降低了认知负载。

完整项目代码见:todo_flutter_harmony

Logo

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

更多推荐