在这里插入图片描述

你有没有过这样的经历:早上出门前想不起来昨天穿了什么,结果连续几天穿了同样的搭配。穿搭日历功能就是用来解决这个问题的,它能记录每天穿了什么,让你的穿搭更有规划。

今天这篇文章,我来详细讲讲衣橱管家App里穿搭日历功能的实现。这个功能用到了日历组件、穿搭记录管理、数据关联查询等技术点。

功能设计思路

穿搭日历功能需要实现以下几点:

第一,展示一个可交互的日历,用户可以选择日期。

第二,在日历上标记有穿搭记录的日期。

第三,选择某一天后,显示当天的穿搭记录。

第四,支持添加新的穿搭记录。

页面基础结构

先看CalendarTab的定义:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart';
import '../../providers/wardrobe_provider.dart';
import '../calendar/add_wear_record_screen.dart';
import '../calendar/wear_history_screen.dart';
import '../calendar/weather_outfit_screen.dart';

class CalendarTab extends StatefulWidget {
  const CalendarTab({super.key});

  
  State<CalendarTab> createState() => _CalendarTabState();
}

class _CalendarTabState extends State<CalendarTab> {
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
}

用StatefulWidget是因为需要维护日历的状态,包括当前显示的月份、选中的日期等。
table_calendar是一个功能强大的日历组件,支持月视图、周视图、事件标记等功能。
_calendarFormat控制日历显示格式,可以是月视图或周视图。
_focusedDay是当前聚焦的日期,决定日历显示哪个月份。
_selectedDay是用户选中的日期,可能为null。

页面布局

build方法构建整个页面:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('穿搭日历'),
      actions: [
        IconButton(
          icon: const Icon(Icons.history),
          onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => const WearHistoryScreen()),
          ),
        ),
        IconButton(
          icon: const Icon(Icons.wb_sunny),
          onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => const WeatherOutfitScreen()),
          ),
        ),
      ],
    ),
    body: Column(
      children: [
        _buildCalendar(),
        Expanded(child: _buildDayRecords()),
      ],
    ),
    floatingActionButton: FloatingActionButton(
      backgroundColor: const Color(0xFFE91E63),
      onPressed: () => Navigator.push(
        context,
        MaterialPageRoute(
          builder: (_) => AddWearRecordScreen(selectedDate: _selectedDay ?? DateTime.now()),
        ),
      ),
      child: const Icon(Icons.add, color: Colors.white),
    ),
  );
}

AppBar右边有两个按钮:历史记录和天气穿搭,方便用户快速访问相关功能。
body分两部分:上面是日历,下面是当天的穿搭记录,用Column和Expanded实现。
FloatingActionButton用来添加新的穿搭记录,点击后跳转到添加页面,并传入当前选中的日期。

日历组件

日历是这个页面的核心:

Widget _buildCalendar() {
  return Consumer<WardrobeProvider>(
    builder: (context, provider, child) {
      return Card(
        margin: EdgeInsets.all(16.w),
        child: TableCalendar(
          firstDay: DateTime.utc(2020, 1, 1),
          lastDay: DateTime.utc(2030, 12, 31),
          focusedDay: _focusedDay,
          calendarFormat: _calendarFormat,
          locale: 'zh_CN',
          headerStyle: HeaderStyle(
            formatButtonVisible: true,
            titleCentered: true,
            formatButtonDecoration: BoxDecoration(
              color: const Color(0xFFE91E63).withOpacity(0.1),
              borderRadius: BorderRadius.circular(8.r),
            ),
          ),
          calendarStyle: CalendarStyle(
            selectedDecoration: const BoxDecoration(
              color: Color(0xFFE91E63),
              shape: BoxShape.circle,
            ),
            todayDecoration: BoxDecoration(
              color: const Color(0xFFE91E63).withOpacity(0.3),
              shape: BoxShape.circle,
            ),
            markerDecoration: const BoxDecoration(
              color: Colors.orange,
              shape: BoxShape.circle,
            ),
          ),
          selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
          eventLoader: (day) {
            return provider.wearRecords
                .where((r) => isSameDay(r.date, day))
                .toList();
          },
          onDaySelected: (selectedDay, focusedDay) {
            setState(() {
              _selectedDay = selectedDay;
              _focusedDay = focusedDay;
            });
          },
          onFormatChanged: (format) {
            setState(() {
              _calendarFormat = format;
            });
          },
        ),
      );
    },
  );
}

firstDay和lastDay定义日历的可选范围,这里设置为2020年到2030年。
locale: 'zh_CN’让日历显示中文,需要在main.dart里配置intl的本地化。
headerStyle配置日历头部样式,formatButtonVisible显示格式切换按钮,titleCentered让标题居中。
calendarStyle配置日历主体样式,selectedDecoration是选中日期的样式,todayDecoration是今天的样式。
markerDecoration是事件标记的样式,有穿搭记录的日期会显示一个小圆点。

事件加载

eventLoader用来加载每天的事件:

eventLoader: (day) {
  return provider.wearRecords
      .where((r) => isSameDay(r.date, day))
      .toList();
},

这个函数会被日历组件调用,传入每一天的日期。
从provider.wearRecords里筛选出当天的记录。
isSameDay是table_calendar提供的工具函数,比较两个日期是否是同一天。
返回的列表长度决定了日期下方显示几个标记点。

日期选择回调

用户点击日期时触发:

onDaySelected: (selectedDay, focusedDay) {
  setState(() {
    _selectedDay = selectedDay;
    _focusedDay = focusedDay;
  });
},

selectedDay是用户点击的日期,focusedDay是当前聚焦的日期。
更新状态后,日历会高亮显示选中的日期,下方会显示当天的穿搭记录。
setState触发页面重建,确保UI和状态同步。

当天穿搭记录

显示选中日期的穿搭记录:

Widget _buildDayRecords() {
  return Consumer<WardrobeProvider>(
    builder: (context, provider, child) {
      final selectedDate = _selectedDay ?? DateTime.now();
      final records = provider.wearRecords
          .where((r) => isSameDay(r.date, selectedDate))
          .toList();

      return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 16.w),
            child: Text(
              DateFormat('yyyy年MM月dd日').format(selectedDate),
              style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
            ),
          ),
          SizedBox(height: 8.h),
          Expanded(
            child: records.isEmpty
                ? _buildEmptyState(selectedDate)
                : ListView.builder(
                    padding: EdgeInsets.symmetric(horizontal: 16.w),
                    itemCount: records.length,
                    itemBuilder: (context, index) {
                      final record = records[index];
                      return _buildRecordCard(record, provider);
                    },
                  ),
          ),
        ],
      );
    },
  );
}

如果_selectedDay为null,默认显示今天的记录。
用DateFormat格式化日期,显示"2024年01月15日"这样的格式。
如果当天没有记录,显示空状态提示;有记录则用ListView展示。
Consumer确保穿搭记录变化时UI能自动更新。

空状态展示

当天没有穿搭记录时的展示:

Widget _buildEmptyState(DateTime selectedDate) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.event_note, size: 48.sp, color: Colors.grey),
        SizedBox(height: 8.h),
        Text('当天无穿搭记录', style: TextStyle(color: Colors.grey)),
        SizedBox(height: 8.h),
        TextButton(
          onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (_) => AddWearRecordScreen(selectedDate: selectedDate),
            ),
          ),
          child: const Text('添加记录'),
        ),
      ],
    ),
  );
}

用图标加文字的方式展示空状态,比空白页面友好。
提供一个"添加记录"按钮,方便用户快速添加。
点击按钮跳转到添加页面,并传入当前选中的日期。

穿搭记录卡片

展示单条穿搭记录:

Widget _buildRecordCard(dynamic record, WardrobeProvider provider) {
  String itemName = '未知';
  if (record.clothingId != null) {
    final item = provider.clothes.firstWhere(
      (c) => c.id == record.clothingId,
      orElse: () => throw Exception(),
    );
    itemName = item.name;
  } else if (record.outfitId != null) {
    final outfit = provider.outfits.firstWhere(
      (o) => o.id == record.outfitId,
      orElse: () => throw Exception(),
    );
    itemName = '搭配: ${outfit.name}';
  }

  return Card(
    margin: EdgeInsets.only(bottom: 8.h),
    child: ListTile(
      leading: CircleAvatar(
        backgroundColor: const Color(0xFFE91E63).withOpacity(0.1),
        child: const Icon(Icons.checkroom, color: Color(0xFFE91E63)),
      ),
      title: Text(itemName),
      subtitle: Row(
        children: [
          if (record.weather != null) ...[
            Icon(Icons.wb_sunny, size: 14.sp, color: Colors.orange),
            SizedBox(width: 4.w),
            Text(record.weather!, style: TextStyle(fontSize: 12.sp)),
            SizedBox(width: 8.w),
          ],
          if (record.mood != null) ...[
            Icon(Icons.mood, size: 14.sp, color: Colors.green),
            SizedBox(width: 4.w),
            Text(record.mood!, style: TextStyle(fontSize: 12.sp)),
          ],
        ],
      ),
    ),
  );
}

穿搭记录可能关联单件衣物或整套搭配,需要根据clothingId或outfitId查询对应的名称。
orElse处理找不到的情况,可能是衣物或搭配被删除了。
subtitle显示天气和心情信息,用图标加文字的方式展示。
…是Dart的展开操作符,配合if使用可以条件性地添加多个Widget。

日历格式切换

用户可以切换月视图和周视图:

onFormatChanged: (format) {
  setState(() {
    _calendarFormat = format;
  });
},

table_calendar支持月视图、双周视图、周视图三种格式。
点击日历头部的格式按钮可以切换。
周视图在手机上更紧凑,月视图能看到更多日期。

日历本地化配置

要让日历显示中文,需要在main.dart里配置:

import 'package:intl/date_symbol_data_local.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDateFormatting('zh_CN', null);
  runApp(const MyApp());
}

initializeDateFormatting初始化日期格式化数据。
'zh_CN’是中文简体的locale代码。
这样日历的星期、月份等都会显示中文。

穿搭记录数据模型

穿搭记录的数据结构:

class WearRecord {
  final String id;
  final DateTime date;
  final String? clothingId;  // 单件衣物ID
  final String? outfitId;    // 搭配ID
  final String? weather;     // 天气
  final String? mood;        // 心情

  WearRecord({
    required this.id,
    required this.date,
    this.clothingId,
    this.outfitId,
    this.weather,
    this.mood,
  });
}

clothingId和outfitId二选一,表示穿的是单件衣物还是整套搭配。
weather和mood是可选的附加信息,记录当天的天气和心情。
这些信息可以用来做穿搭分析,比如什么天气穿什么衣服。

isSameDay的实现

比较两个日期是否是同一天:

bool isSameDay(DateTime? a, DateTime? b) {
  if (a == null || b == null) return false;
  return a.year == b.year && a.month == b.month && a.day == b.day;
}

只比较年、月、日,忽略时、分、秒。
处理null的情况,两个都是null返回false。
table_calendar包里已经提供了这个函数,直接用就行。

添加穿搭记录

点击FloatingActionButton跳转到添加页面:

FloatingActionButton(
  backgroundColor: const Color(0xFFE91E63),
  onPressed: () => Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => AddWearRecordScreen(selectedDate: _selectedDay ?? DateTime.now()),
    ),
  ),
  child: const Icon(Icons.add, color: Colors.white),
)

传入当前选中的日期,这样添加页面知道要为哪一天添加记录。
如果没有选中日期,默认用今天。
添加成功后返回,日历上会显示新的标记点。

总结

穿搭日历功能的实现涉及到日历组件、事件标记、数据关联查询等多个方面。关键点在于:

用table_calendar组件实现可交互的日历,支持日期选择和事件标记。

用eventLoader加载每天的穿搭记录,在日历上显示标记点。

选中日期后显示当天的穿搭记录,支持查看详情和添加新记录。

在OpenHarmony平台上,这套实现方式完全适用。穿搭日历是一个很实用的功能,能帮助用户回顾和规划自己的穿搭。

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐