|
@@ -0,0 +1,336 @@
|
|
|
|
+import 'package:cs_resources/generated/assets.dart';
|
|
|
|
+import 'package:flutter/material.dart';
|
|
|
|
+import 'package:intl/intl.dart';
|
|
|
|
+import 'package:shared/utils/log_utils.dart';
|
|
|
|
+import 'package:widgets/ext/ex_widget.dart';
|
|
|
|
+import 'package:widgets/my_load_image.dart';
|
|
|
|
+
|
|
|
|
+//日历的具体展示
|
|
|
|
+class FullCalendar extends StatefulWidget {
|
|
|
|
+ final DateTime startDate;
|
|
|
|
+ final DateTime? endDate;
|
|
|
|
+ final DateTime? selectedDate;
|
|
|
|
+ final Color? dateColor;
|
|
|
|
+ final Color? dateSelectedColor;
|
|
|
|
+ final Color? dateSelectedBg;
|
|
|
|
+ final double? padding;
|
|
|
|
+ final String? locale;
|
|
|
|
+ final Widget? calendarBackground;
|
|
|
|
+ final List<String>? events;
|
|
|
|
+ final Function onDateChange;
|
|
|
|
+
|
|
|
|
+ const FullCalendar({
|
|
|
|
+ Key? key,
|
|
|
|
+ this.endDate,
|
|
|
|
+ required this.startDate,
|
|
|
|
+ required this.padding,
|
|
|
|
+ required this.onDateChange,
|
|
|
|
+ this.calendarBackground,
|
|
|
|
+ this.events,
|
|
|
|
+ this.dateColor,
|
|
|
|
+ this.dateSelectedColor,
|
|
|
|
+ this.dateSelectedBg,
|
|
|
|
+ this.locale,
|
|
|
|
+ this.selectedDate,
|
|
|
|
+ }) : super(key: key);
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ State<FullCalendar> createState() => _FullCalendarState();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+class _FullCalendarState extends State<FullCalendar> {
|
|
|
|
+ late DateTime endDate;
|
|
|
|
+
|
|
|
|
+ late DateTime startDate;
|
|
|
|
+ late int _initialPage;
|
|
|
|
+
|
|
|
|
+ List<String>? _events = [];
|
|
|
|
+
|
|
|
|
+ late PageController _horizontalScroll;
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ void initState() {
|
|
|
|
+ setState(() {
|
|
|
|
+ startDate = DateTime.parse("${widget.startDate.toString().split(" ").first} 00:00:00.000");
|
|
|
|
+
|
|
|
|
+ endDate = DateTime.parse("${widget.endDate.toString().split(" ").first} 23:00:00.000");
|
|
|
|
+
|
|
|
|
+ _events = widget.events;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ super.initState();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
+ List<String> partsStart = startDate.toString().split(" ").first.split("-");
|
|
|
|
+
|
|
|
|
+ DateTime firstDate = DateTime.parse("${partsStart.first}-${partsStart[1].padLeft(2, '0')}-01 00:00:00.000");
|
|
|
|
+
|
|
|
|
+ List<String> partsEnd = endDate.toString().split(" ").first.split("-");
|
|
|
|
+
|
|
|
|
+ DateTime lastDate =
|
|
|
|
+ DateTime.parse("${partsEnd.first}-${(int.parse(partsEnd[1]) + 1).toString().padLeft(2, '0')}-01 23:00:00.000").subtract(const Duration(days: 1));
|
|
|
|
+
|
|
|
|
+ double width = MediaQuery.of(context).size.width - (2 * widget.padding!);
|
|
|
|
+
|
|
|
|
+ List<DateTime?> dates = [];
|
|
|
|
+
|
|
|
|
+ DateTime referenceDate = firstDate;
|
|
|
|
+
|
|
|
|
+ while (referenceDate.isBefore(lastDate)) {
|
|
|
|
+ List<String> referenceParts = referenceDate.toString().split(" ");
|
|
|
|
+ DateTime newDate = DateTime.parse("${referenceParts.first} 12:00:00.000");
|
|
|
|
+ dates.add(newDate);
|
|
|
|
+
|
|
|
|
+ referenceDate = newDate.add(const Duration(days: 1));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (firstDate.year == lastDate.year && firstDate.month == lastDate.month) {
|
|
|
|
+ return Padding(
|
|
|
|
+ padding: EdgeInsets.fromLTRB(widget.padding!, 40.0, widget.padding!, 0.0),
|
|
|
|
+ child: month(dates, width, widget.locale),
|
|
|
|
+ );
|
|
|
|
+ } else {
|
|
|
|
+ List<DateTime?> months = [];
|
|
|
|
+ for (int i = 0; i < dates.length; i++) {
|
|
|
|
+ if (i == 0 || (dates[i]!.month != dates[i - 1]!.month)) {
|
|
|
|
+ months.add(dates[i]);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ months.sort((b, a) => a!.compareTo(b!));
|
|
|
|
+
|
|
|
|
+ final index = months.indexWhere((element) => element!.month == widget.selectedDate!.month && element.year == widget.selectedDate!.year);
|
|
|
|
+
|
|
|
|
+ _initialPage = index;
|
|
|
|
+ _horizontalScroll = PageController(initialPage: _initialPage);
|
|
|
|
+
|
|
|
|
+ double monthHeight = 6 * (width / 7) + 16 + 10 + 10 + 80; // 固定高度,6行的高度加上80额外空间
|
|
|
|
+
|
|
|
|
+ return Container(
|
|
|
|
+ height: monthHeight, // 使用固定的高度
|
|
|
|
+ padding: const EdgeInsets.fromLTRB(25, 10.0, 25, 20.0),
|
|
|
|
+ //只是PageView
|
|
|
|
+ child: Stack(
|
|
|
|
+ children: [
|
|
|
|
+ //主题
|
|
|
|
+ PageView.builder(
|
|
|
|
+ physics: const BouncingScrollPhysics(),
|
|
|
|
+ controller: _horizontalScroll,
|
|
|
|
+ reverse: true,
|
|
|
|
+ scrollDirection: Axis.horizontal,
|
|
|
|
+ itemCount: months.length,
|
|
|
|
+ itemBuilder: (context, index) {
|
|
|
|
+ DateTime? date = months[index];
|
|
|
|
+ List<DateTime?> daysOfMonth = [];
|
|
|
|
+ for (var item in dates) {
|
|
|
|
+ if (date!.month == item!.month && date.year == item.year) {
|
|
|
|
+ daysOfMonth.add(item);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool isLast = index == 0;
|
|
|
|
+
|
|
|
|
+ return Container(
|
|
|
|
+ padding: EdgeInsets.only(bottom: isLast ? 0.0 : 10.0),
|
|
|
|
+ child: month(daysOfMonth, width, widget.locale),
|
|
|
|
+ );
|
|
|
|
+ },
|
|
|
|
+ ),
|
|
|
|
+
|
|
|
|
+ //返回按钮
|
|
|
|
+ Positioned(
|
|
|
|
+ top: -11,
|
|
|
|
+ width: MediaQuery.of(context).size.width * 0.88,
|
|
|
|
+ child: Row(
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
+ children: [
|
|
|
|
+ const MyAssetImage(
|
|
|
|
+ Assets.baseLibCalendarLeftIcon,
|
|
|
|
+ width: 44,
|
|
|
|
+ height: 44,
|
|
|
|
+ ).onTap(() {
|
|
|
|
+ _horizontalScroll.nextPage(
|
|
|
|
+ duration: const Duration(milliseconds: 300),
|
|
|
|
+ curve: Curves.ease,
|
|
|
|
+ );
|
|
|
|
+ }),
|
|
|
|
+ const MyAssetImage(
|
|
|
|
+ Assets.baseLibCalendarRightIcon,
|
|
|
|
+ width: 44,
|
|
|
|
+ height: 44,
|
|
|
|
+ ).onTap(() {
|
|
|
|
+ _horizontalScroll.previousPage(
|
|
|
|
+ duration: const Duration(milliseconds: 300),
|
|
|
|
+ curve: Curves.ease,
|
|
|
|
+ );
|
|
|
|
+ }),
|
|
|
|
+ ],
|
|
|
|
+ ),
|
|
|
|
+ )
|
|
|
|
+ ],
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //顶部星期的文本数据
|
|
|
|
+ Widget daysOfWeek(double width, String? locale) {
|
|
|
|
+ List daysNames = [];
|
|
|
|
+ for (var day = 12; day <= 18; day++) {
|
|
|
|
+ daysNames.add(DateFormat.E(locale.toString()).format(DateTime.parse('1970-01-$day')));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return Row(
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
+ children: [
|
|
|
|
+ dayName(width / 7, daysNames[0]),
|
|
|
|
+ dayName(width / 7, daysNames[1]),
|
|
|
|
+ dayName(width / 7, daysNames[2]),
|
|
|
|
+ dayName(width / 7, daysNames[3]),
|
|
|
|
+ dayName(width / 7, daysNames[4]),
|
|
|
|
+ dayName(width / 7, daysNames[5]),
|
|
|
|
+ dayName(width / 7, daysNames[6]),
|
|
|
|
+ ],
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //顶部星期的文本控件展示
|
|
|
|
+ Widget dayName(double width, String text) {
|
|
|
|
+ return Container(
|
|
|
|
+ width: width,
|
|
|
|
+ alignment: Alignment.center,
|
|
|
|
+ child: Text(
|
|
|
|
+ text,
|
|
|
|
+ style: const TextStyle(
|
|
|
|
+ fontSize: 13.0,
|
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
|
+ ),
|
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //当前月份,每一天的布局
|
|
|
|
+ Widget dateInCalendar(DateTime date, bool outOfRange, double width, bool event) {
|
|
|
|
+ bool isSelectedDate = date.toString().split(" ").first == widget.selectedDate.toString().split(" ").first;
|
|
|
|
+ return GestureDetector(
|
|
|
|
+ onTap: () => outOfRange ? null : widget.onDateChange(date),
|
|
|
|
+ child: Container(
|
|
|
|
+ width: width / 7,
|
|
|
|
+ height: width / 7,
|
|
|
|
+ decoration: BoxDecoration(
|
|
|
|
+ shape: BoxShape.circle,
|
|
|
|
+ color: isSelectedDate ? widget.dateSelectedBg : Colors.transparent,
|
|
|
|
+ ),
|
|
|
|
+ alignment: Alignment.center,
|
|
|
|
+ child: Column(
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
+ children: [
|
|
|
|
+ const SizedBox(
|
|
|
|
+ height: 5.0,
|
|
|
|
+ ),
|
|
|
|
+ Padding(
|
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
|
|
+ child: Text(
|
|
|
|
+ DateFormat("dd").format(date),
|
|
|
|
+ style: TextStyle(
|
|
|
|
+ color: outOfRange
|
|
|
|
+ ? isSelectedDate
|
|
|
|
+ ? widget.dateSelectedColor!.withOpacity(0.9)
|
|
|
|
+ : widget.dateColor!.withOpacity(0.4)
|
|
|
|
+ : isSelectedDate
|
|
|
|
+ ? widget.dateSelectedColor
|
|
|
|
+ : widget.dateColor,
|
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
|
+ fontSize: 13),
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ event
|
|
|
|
+ ? Icon(
|
|
|
|
+ Icons.bookmark,
|
|
|
|
+ size: 8,
|
|
|
|
+ color: isSelectedDate ? widget.dateSelectedColor : widget.dateSelectedBg,
|
|
|
|
+ )
|
|
|
|
+ : const SizedBox(height: 5.0),
|
|
|
|
+ ],
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //单独一个月的Page布局
|
|
|
|
+ Widget month(List dates, double width, String? locale) {
|
|
|
|
+ DateTime first = dates.first;
|
|
|
|
+
|
|
|
|
+ // 获取这个月的第一天
|
|
|
|
+ DateTime firstDayOfMonth = DateTime(first.year, first.month, 1);
|
|
|
|
+
|
|
|
|
+ // 找到这个月的第一天是星期几
|
|
|
|
+ int firstWeekday = firstDayOfMonth.weekday;
|
|
|
|
+
|
|
|
|
+ // 计算需要的前导空格数量
|
|
|
|
+ int leadingDaysCount = (firstWeekday - DateTime.monday + 7) % 7;
|
|
|
|
+
|
|
|
|
+ // 只保留当前月份的日期
|
|
|
|
+ List<DateTime?> fullDates = List.from(dates);
|
|
|
|
+
|
|
|
|
+ // 在视图中添加用于填充的空日期(如果需要,这里就不填充上个月尾的日期了)
|
|
|
|
+ for (int i = 0; i < leadingDaysCount; i++) {
|
|
|
|
+ fullDates.insert(0, null); // 用null填充前导位置
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return Column(
|
|
|
|
+ mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
+ children: [
|
|
|
|
+ Text(
|
|
|
|
+ DateFormat.yMMMM(Locale(locale!).toString()).format(first),
|
|
|
|
+ style: TextStyle(fontSize: 18.0, color: widget.dateColor, fontWeight: FontWeight.w500),
|
|
|
|
+ ),
|
|
|
|
+
|
|
|
|
+ // 周一到周天的星期文本
|
|
|
|
+ Padding(
|
|
|
|
+ padding: const EdgeInsets.only(top: 30.0),
|
|
|
|
+ child: daysOfWeek(width, widget.locale),
|
|
|
|
+ ),
|
|
|
|
+
|
|
|
|
+ // 底部的每月的每一天
|
|
|
|
+ Container(
|
|
|
|
+ padding: const EdgeInsets.only(top: 10.0),
|
|
|
|
+ height: (fullDates.length > 28)
|
|
|
|
+ ? (fullDates.length > 35 ? 6.2 * width / 7 : 5.2 * width / 7)
|
|
|
|
+ : 4 * width / 7,
|
|
|
|
+ width: MediaQuery.of(context).size.width - 2 * widget.padding!,
|
|
|
|
+ child: GridView.builder(
|
|
|
|
+ itemCount: fullDates.length,
|
|
|
|
+ physics: const NeverScrollableScrollPhysics(),
|
|
|
|
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 7),
|
|
|
|
+ itemBuilder: (context, index) {
|
|
|
|
+ DateTime? date = fullDates[index]; // 使用 DateTime? 类型以支持 null
|
|
|
|
+
|
|
|
|
+ // 如果 date 为 null,表示该位置为空,返回一个透明的容器
|
|
|
|
+ if (date == null) {
|
|
|
|
+ return Container(
|
|
|
|
+ width: width / 7,
|
|
|
|
+ height: width / 7,
|
|
|
|
+ color: Colors.transparent, // 透明或其他样式
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bool outOfRange = date.isBefore(startDate) || date.isAfter(endDate);
|
|
|
|
+
|
|
|
|
+ return dateInCalendar(
|
|
|
|
+ date,
|
|
|
|
+ outOfRange,
|
|
|
|
+ width,
|
|
|
|
+ _events!.contains(date.toString().split(" ").first) && !outOfRange,
|
|
|
|
+ );
|
|
|
|
+ },
|
|
|
|
+ ),
|
|
|
|
+ )
|
|
|
|
+ ],
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}
|