一天时间,用Flutter完成一款打工人加班工时计算工具

456 阅读8分钟

公司最近改为了弹性考勤制度,不含午休时间一天要干8小时,多出的工时每4小时算半天加班。考虑到苍蝇腿也是肉,避免3.9小时的尴尬,使用GetxScaffold框架花1天时间写了这个打工人工时计算工具。也算是给使用GetxScaffold脚手架的同学做个示例,有不同需求的同学也可以根据自己的需求自行更改。

img1.png

关于GetxScaffold

GetXScaffold 快速开发脚手架在 GetX 框架和一些常用插件的基础上,构建了一套完整的快速开发模板。其中包括新增了部分常用功能的全局方法、常用的扩展方法和各种工具类、部分常用组件的封装、简单易用的对话框、二次封装的 Dio 网络请求工具、二次封装的 GetxController、二次封装的应用主题和国际化实现等。GetXScaffold 是对以上这些内容的过度封装,包括一些组件的扩展方法会违背 Flutter 本身的开发规范,改变你的开发习惯。所以本脚手架单纯为了提高开发效率,减少重复代码,减少开发成本。如果您是刚接触 Flutter 开发并还处在学习过程中的话,并不推荐您使用该脚手架。以下只是部分功能的使用示例,建议您通过示例项目或者源码了解全部使用方法。

Pub.dev地址      Github地址

关于WorkHelper

应用每天首次打开则记录当前时间为上班时间,再次打开或者点击右上方按钮则更新下班时间。左侧日历则统计每日工时。可根据自己情况设置午休时间,每月应上班的天数和每日的工时,下方做相关统计。纯纯牛马!!!

Github地址

项目依赖

/pubspec.yaml

因项目依赖GetxScaffold,如本地已下载GetxScaffold源码,请修改 pubspec.yaml 文件里的 getx_scaffold 路径。如果未下载GetxScaffold源码,将 getx_scaffold 修改为Pub仓库地址。

name: work_helper
description: "A new Flutter project."
publish_to: "none"

version: 1.0.0+1

environment:
  sdk: ^3.5.1

dependencies:
  flutter:
    sdk: flutter
  getx_scaffold:
    path: ../getx_scaffold
  window_manager: ^0.4.2
  calendar_view: ^1.2.0
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  time_pickerr: ^1.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0
  icons_launcher: ^3.0.0

# flutter_icons 应用启动图标配置 https://pub-web.flutter-io.cn/packages/icons_launcher
# flutter pub run icons_launcher:create
icons_launcher:
  platforms:
    windows:
      enable: true
      image_path: "assets/icons/ic_windows.png"
    macos:
      enable: true
      image_path: "assets/icons/ic_macos.png"

flutter:
  uses-material-design: true

  assets:
    - assets/icons/

  fonts:
    - family: Montserrat
      fonts:
        - asset: assets/fonts/Montserrat-Thin.ttf
          weight: 100
        - asset: assets/fonts/Montserrat-ExtraLight.ttf
          weight: 200
        - asset: assets/fonts/Montserrat-Light.ttf
          weight: 300
        - asset: assets/fonts/Montserrat-Regular.ttf
          weight: 400
        - asset: assets/fonts/Montserrat-Medium.ttf
          weight: 500
        - asset: assets/fonts/Montserrat-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Montserrat-Bold.ttf
          weight: 700
        - asset: assets/fonts/Montserrat-SemiBold.ttf
          weight: 800
        - asset: assets/fonts/Montserrat-Black.ttf
          weight: 900
    - family: AlibabaHealth
      fonts:
        - asset: assets/fonts/AlibabaHealthFont2.0CN-45R.ttf
          weight: 400
        - asset: assets/fonts/AlibabaHealthFont2.0CN-85B.ttf
          weight: 700

目录结构

因为所有的工具类和扩展都集成在GetxScaffold框架中,项目目录非常简洁。

lib -> home-> | controller.dart //控制器
     |        | index.dart
     |        | view.dart  //视图
     | main.dart   //入口文件
     | theme.dart  //主题文件
        

/lib/main.dart

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:window_manager/window_manager.dart';
import 'package:work_helper/home/index.dart';
import 'package:work_helper/theme.dart';

const String APP_NAME = 'WorkHelper';
const double WINDOW_WIDTH = 1600;
const double WINDOW_HEIGHT = 1050;

void main() async {
  //框架初始化
  await init(
    isDebug: kDebugMode,
    logTag: APP_NAME,
  );
  await windowManager.ensureInitialized();
  await Hive.initFlutter();
  Size size = const Size(WINDOW_WIDTH, WINDOW_HEIGHT);

  WindowOptions windowOptions = WindowOptions(
    size: size,
    minimumSize: size,
    center: true,
    backgroundColor: Platform.isWindows ? Colors.transparent : Colors.white,
    skipTaskbar: false,
    titleBarStyle: TitleBarStyle.normal,
    title: APP_NAME,
  );
  windowManager.waitUntilReadyToShow(windowOptions, () async {
    await windowManager.show();
    await windowManager.focus();
  });
  runApp(
    GetxApp(
      // 设计尺寸
      designSize: const Size(WINDOW_WIDTH, WINDOW_HEIGHT),
      // Debug Banner
      debugShowCheckedModeBanner: false,
      // Getx Log
      enableLog: kDebugMode,
      // 默认的跳转动画
      defaultTransition: Transition.fadeIn,
      // 主题模式
      themeMode: GlobalService.to.themeMode,
      // 主题
      theme: AppTheme.light,
      // AppTitle
      title: APP_NAME,
      // 首页
      home: const HomePage(),
      // Builder
      builder: (context, widget) {
        return widget!;
      },
    ),
  );
}


/lib/theme.dart

import 'dart:io';

import 'package:flutter/material.dart';

class AppTheme {
  static const String Font_Montserrat = 'Montserrat';

  static const String Font_AlibabaHealth = 'AlibabaHealth';

  static const Color themeColor = Color.fromARGB(255, 10, 53, 205);

  static const Color secondaryColor = Colors.orange;

  /// 亮色主题样式
  static ThemeData light = ThemeData(
    useMaterial3: false,
    fontFamily: Platform.isWindows ? Font_AlibabaHealth : Font_Montserrat,
    colorScheme: ColorScheme.fromSeed(
      seedColor: themeColor,
      primary: themeColor,
      secondary: secondaryColor,
      brightness: Brightness.light,
      surface: Colors.white,
      surfaceTint: Colors.transparent,
    ),
    appBarTheme: const AppBarTheme(
      backgroundColor: Colors.white,
      foregroundColor: Color.fromARGB(200, 0, 0, 0),
      centerTitle: true,
      titleTextStyle: TextStyle(
        fontSize: 18,
        fontWeight: FontWeight.bold,
        color: Color.fromARGB(200, 0, 0, 0),
      ),
    ),
  );
}


/lib/home/view.dart

import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:time_pickerr/time_pickerr.dart';
import 'package:work_helper/theme.dart';

import 'index.dart';

class HomePage extends GetView<HomeController> {
  const HomePage({super.key});

  // 主视图
  Widget _buildView() {
    return <Widget>[
      MonthView(
        controller: controller.eventController,
        minMonth: DateTime(2024),
        maxMonth: DateTime(2050),
        cellAspectRatio: 1,
        startDay: WeekDays.monday,
        initialMonth: DateTime.now(),
        showWeekTileBorder: false,
        hideDaysNotInMonth: true,
        showBorder: false,
        headerStyle: HeaderStyle(
            headerTextStyle: TextStyle(
              fontSize: 20.sp,
              fontFamily: AppTheme.Font_Montserrat,
            ),
            decoration: const BoxDecoration(
              color: Colors.white,
            )),
        onHeaderTitleTap: (date) async {
          return;
        },
        cellBuilder: _buildCell,
        weekDayStringBuilder: (week) {
          switch (week) {
            case 0:
              return '星期一';
            case 1:
              return '星期二';
            case 2:
              return '星期三';
            case 3:
              return '星期四';
            case 4:
              return '星期五';
            case 5:
              return '星期六';
            case 6:
              return '星期天';
            default:
              return '';
          }
        },
        onPageChange: (date, pageIndex) {
          controller.onPageChange(date);
        },
        onCellTap: (events, date) {},
      ).tight(
        width: 0.67.sw,
        height: 1.sh,
      ),
      Container(
        color: Colors.grey[300],
        width: 2.w,
        height: 1.sh,
      ),
      _buildRightViews().expand(),
    ].toRow();
  }

  Widget _buildCell(
    DateTime date,
    List<CalendarEventData<Object?>> events,
    bool isToday,
    bool isInMonth,
    bool hideDaysNotInMonth,
  ) {
    if (!isInMonth) {
      return Container();
    }
    DateTime cellTime = DateTime.parse('${date.toDateString()} 00:00:00');
    DateTime nowTime = DateTime.parse('${getNowDateString()} 00:00:00');
    int tag;
    if (isToday) {
      tag = 0;
    } else if (cellTime.isBefore(nowTime)) {
      tag = -1;
    } else {
      tag = 1;
    }
    String? start;
    String? end;
    if (events.isNotEmpty) {
      var event = events[0];
      Map map = event.event as Map;
      start = map['start'] as String?;
      end = map['end'] as String?;
    }
    Widget widget = <Widget>[
      _buildCellTitle(
        date.dateFormat('d'),
        tag,
        start,
        end,
      ),
      8.verticalSpace,
      if (start != null)
        TextX.labelMedium(
          '上班:$start',
          color: Colors.green,
          weight: FontWeight.bold,
        ),
      2.verticalSpace,
      if (end != null)
        TextX.labelMedium(
          '下班:$end',
          color: Colors.green,
          weight: FontWeight.bold,
        ),
      2.verticalSpace,
      if (start != null && end != null)
        TextX.labelMedium(
          '工时:${controller.getWorkDurationString(start, end)}',
          color: Colors.orange,
          weight: FontWeight.bold,
        ),
      2.verticalSpace,
      if (start != null && end != null)
        TextX.labelMedium(
          '分钟:${controller.getWorkDurationMinutes(start, end)}',
          color: Colors.orange,
          weight: FontWeight.bold,
        ),
    ].toColumn(crossAxisAlignment: CrossAxisAlignment.start).padding(all: 10.w);
    if (tag < 0) {
      //之前的时间可以补卡
      return widget.inkWell(
        onTap: () {
          Get.dialog(_buildRemedyDialog(date, start, end));
        },
      ).card();
    } else {
      return widget.card();
    }
  }

  Widget _buildRemedyDialog(DateTime date, String? start, String? end) {
    Rx<String?> startTime = Rx<String?>(start);
    Rx<String?> endTime = Rx<String?>(end);
    return <Widget>[
      Obx(() {
        return <Widget>[
          TextX.titleMedium(
            date.toDateString(),
            weight: FontWeight.bold,
          ),
          20.verticalSpace,
          <Widget>[
            TextX.titleLarge('上班时间:${startTime.value ?? ''}'),
            ButtonX.secondary(
              '修改',
              onPressed: () {
                Get.dialog(
                  CustomHourPicker(
                    title: '请选择上班时间',
                    initDate: controller.timeToDateTime(startTime.value ?? '08:30:00'),
                    date: controller.timeToDateTime(startTime.value ?? '08:30:00'),
                    elevation: 2,
                    positiveButtonText: '确认修改',
                    negativeButtonText: '取消',
                    onPositivePressed: (context, time) {
                      startTime.value = time.toTimeString();
                      controller.startTimeRemedy(date, time);
                      Get.back();
                    },
                    onNegativePressed: (context) {
                      Get.back();
                    },
                  ).padding(horizontal: 600.w),
                );
              },
            ).padding(left: 20.w),
          ].toRow(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
          ),
          10.verticalSpace,
          <Widget>[
            TextX.titleLarge('下班时间:${endTime.value ?? ''}'),
            ButtonX.secondary(
              '修改',
              onPressed: () {
                if (startTime.value == null) {
                  showError('请先补卡上班时间');
                  return;
                }
                Get.dialog(
                  CustomHourPicker(
                    title: '请选择下班时间',
                    initDate: controller.timeToDateTime(endTime.value ?? '18:30:00'),
                    date: controller.timeToDateTime(endTime.value ?? '18:30:00'),
                    elevation: 2,
                    positiveButtonText: '确认修改',
                    negativeButtonText: '取消',
                    onPositivePressed: (context, time) {
                      Get.back();
                      if (startTime.value == time.toTimeString()) {
                        showError('上班时间不能与下班时间相同');
                        return;
                      }
                      if (controller
                          .timeToDateTime(startTime.value)
                          .isAfter(controller.timeToDateTime(time.toTimeString()))) {
                        showError('上班时间不能在下班时间之后');
                        return;
                      }
                      endTime.value = time.toTimeString();
                      controller.endTimeRemedy(date, time);
                    },
                    onNegativePressed: (context) {
                      Get.back();
                    },
                  ).padding(horizontal: 600.w),
                );
              },
            ).padding(left: 20.w),
          ].toRow(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
          ),
        ]
            .toColumn(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisSize: MainAxisSize.min,
            )
            .padding(top: 25.h, horizontal: 30.w);
      }),
      ButtonX.icon(
        Icons.close,
        onPressed: () => Get.back(),
      ).alignTopRight(),
      ButtonX.outline(
        '清空今日数据',
        textSize: 16.sp,
        foregroundColor: Colors.red,
        onPressed: () {
          startTime.value = null;
          endTime.value = null;
          controller.cleanData(date);
        },
      ).alignBottomCenter(),
    ].toStack().padding(all: 10.w).tight(width: 400.w, height: 230.h).card().center();
  }

  Widget _buildCellTitle(
    String text,
    int tag,
    String? start,
    String? end,
  ) {
    switch (tag) {
      case -1:
        Widget widget;
        if (start == null || end == null) {
          widget = <Widget>[
            TextX.titleMedium(
              text,
              color: Colors.red,
              weight: FontWeight.bold,
            ),
            TextX.labelMedium(
              '缺卡',
              color: Colors.red,
              weight: FontWeight.bold,
            ),
          ].toRow(mainAxisAlignment: MainAxisAlignment.spaceBetween);
        } else {
          widget = TextX.titleMedium(
            text,
            color: Colors.green,
            weight: FontWeight.bold,
          );
        }
        return widget;
      case 0:
        return TextX.titleMedium(
          text,
          color: AppTheme.themeColor,
          weight: FontWeight.bold,
        );
      case 1:
        return TextX.titleMedium(
          text,
          weight: FontWeight.w300,
        );
      default:
        return const SizedBox();
    }
  }

  Widget _buildRightViews() {
    return Obx(
      () => <Widget>[
        <Widget>[
          TextX.headlineSmall(
            '当前时间:${controller.currentTime.value}',
            weight: FontWeight.bold,
            color: AppTheme.themeColor,
          ),
          15.verticalSpace,
          <Widget>[
            TextX.titleLarge('今日上班时间:${controller.todayStartTime.value}'),
            ButtonX.secondary(
              '修改',
              onPressed: () {
                Get.dialog(
                  CustomHourPicker(
                    title: '请选择今日上班时间',
                    initDate: controller.timeToDateTime(controller.todayStartTime.value),
                    date: controller.timeToDateTime(controller.todayStartTime.value),
                    elevation: 2,
                    positiveButtonText: '确认修改',
                    negativeButtonText: '取消',
                    onPositivePressed: (context, time) {
                      controller.updateStartTime(time);
                      Get.back();
                    },
                    onNegativePressed: (context) {
                      Get.back();
                    },
                  ).padding(horizontal: 600.w),
                );
              },
            ).padding(left: 20.w),
          ].toRow(),
          5.verticalSpace,
          TextX.titleLarge('今日下班时间:${controller.todayEndTime.value}'),
          20.verticalSpace,
          ButtonX.primary(
            '更新下班时间',
            textSize: 22.sp,
            onPressed: () => controller.updateEndTime(),
          ),
        ]
            .toColumn(
              crossAxisAlignment: CrossAxisAlignment.start,
            )
            .paddingAll(20.w)
            .card()
            .width(double.infinity),
        5.verticalSpace,
        <Widget>[
          <Widget>[
            TextX.titleLarge('午休开始时间:'),
            TextX.titleLarge(controller.noonBreakStart.value.toTimeString()),
            ButtonX.secondary(
              '修改',
              onPressed: () {
                Get.dialog(
                  CustomHourPicker(
                    title: '请选择午休开始时间',
                    initDate: controller.noonBreakStart.value,
                    date: controller.noonBreakStart.value,
                    elevation: 2,
                    positiveButtonText: '确认修改',
                    negativeButtonText: '取消',
                    onPositivePressed: (context, time) {
                      DateTime start = controller.timeToDateTime(time.toTimeString());
                      DateTime end =
                          controller.timeToDateTime(controller.noonBreakEnd.value.toTimeString());

                      if (start.isBefore(end)) {
                        controller.noonBreakStart.value = time;
                        controller.updateNoonBreakDuration();
                        controller.updateEvent();
                        setValue(HomeController.NOON_BREAK_START, time.toDateTimeString());
                        Get.back();
                      } else {
                        showError('午休开始时间要在结束时间之前');
                      }
                    },
                    onNegativePressed: (context) {
                      Get.back();
                    },
                  ).padding(horizontal: 600.w),
                );
              },
            ).padding(left: 20.w),
          ].toRow(crossAxisAlignment: CrossAxisAlignment.center),
          5.verticalSpace,
          <Widget>[
            TextX.titleLarge('午休结束时间:'),
            TextX.titleLarge(controller.noonBreakEnd.value.toTimeString()),
            ButtonX.secondary(
              '修改',
              onPressed: () {
                Get.dialog(
                  CustomHourPicker(
                    title: '请选择午休结束时间',
                    initDate: controller.noonBreakEnd.value,
                    date: controller.noonBreakEnd.value,
                    elevation: 2,
                    positiveButtonText: '确认修改',
                    negativeButtonText: '取消',
                    onPositivePressed: (context, time) {
                      DateTime start =
                          controller.timeToDateTime(controller.noonBreakStart.value.toTimeString());
                      DateTime end = controller.timeToDateTime(time.toTimeString());
                      if (end.isAfter(start)) {
                        controller.noonBreakEnd.value = time;
                        controller.updateNoonBreakDuration();
                        controller.updateEvent();
                        setValue(HomeController.NOON_BREAK_END, time.toDateTimeString());
                        Get.back();
                      } else {
                        showError('午休结束时间要在开始时间之后');
                      }
                    },
                    onNegativePressed: (context) {
                      Get.back();
                    },
                  ).padding(horizontal: 600.w),
                );
              },
            ).padding(left: 20.w),
          ].toRow(crossAxisAlignment: CrossAxisAlignment.center),
          5.verticalSpace,
          TextX.titleLarge('午休时长:${controller.noonBreakDuration.value}'),
        ]
            .toColumn(
              crossAxisAlignment: CrossAxisAlignment.start,
            )
            .paddingAll(20.w)
            .card()
            .width(double.infinity),
        5.verticalSpace,
        <Widget>[
          TextField(
            controller: controller.workDaysController,
            keyboardType: TextInputType.number, // 只允许数字键盘
            maxLength: 2,
            inputFormatters: [
              FilteringTextInputFormatter.digitsOnly, // 只允许输入数字
            ],
            decoration: InputDecoration(
              labelText: '当月应上班天数',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r), // 设置圆角
                borderSide: BorderSide(color: AppTheme.themeColor, width: 2.sp),
              ),
              filled: true, // 是否填充背景色
              fillColor: Colors.grey[100], // 填充背景色
              suffix: ButtonX.secondary(
                '保存',
                onPressed: controller.workDaysSave,
              ),
              counter: Container(), // 隐藏字符计数
            ),
          ),
          20.verticalSpace,
          TextField(
            controller: controller.workHoursController,
            keyboardType: TextInputType.number, // 只允许数字键盘
            maxLength: 2,
            inputFormatters: [
              FilteringTextInputFormatter.digitsOnly, // 只允许输入数字
            ],
            decoration: InputDecoration(
              labelText: '每日工作时长',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r), // 设置圆角
                borderSide: BorderSide(color: AppTheme.themeColor, width: 2.sp),
              ),
              filled: true, // 是否填充背景色
              fillColor: Colors.grey[100], // 填充背景色
              suffix: ButtonX.secondary(
                '保存',
                onPressed: controller.workHoursSave,
              ),
              counter: Container(), // 隐藏字符计数
            ),
          ),
          20.verticalSpace,
          TextX.titleLarge('当月满勤工时(分钟):${controller.getWorkTimeMinutes()}'),
          5.verticalSpace,
          TextX.titleLarge('当月满勤工时(小时):${controller.getWorkTimeMinutes() / 60}'),
          20.verticalSpace,
          TextX.titleLarge('当月有效工时(分钟):${controller.countMinutes.value}'),
          5.verticalSpace,
          TextX.titleLarge('当月有效工时(小时):${(controller.countMinutes.value / 60).toStringAsFixed(2)}'),
          20.verticalSpace,
          TextX.titleLarge('当月加班工时(分钟):${controller.overtimeMinutes()}'),
          5.verticalSpace,
          TextX.titleLarge('当月加班工时(小时):${(controller.overtimeMinutes() / 60).toStringAsFixed(2)}'),
          10.verticalSpace,
          TextX.titleLarge(
            '当月加班工时(天数):${(controller.overtimeMinutes() / 60 / controller.workHours.value).toStringAsFixed(2)}',
            color: Colors.red,
            weight: FontWeight.bold,
          ),
        ]
            .toColumn(
              crossAxisAlignment: CrossAxisAlignment.start,
            )
            .paddingAll(20.w)
            .card()
            .width(double.infinity),
        TextX.bodyMedium('powered by Kxmrg').padding(top: 10.w).center(),
        TextX.bodySmall('Ver.${controller.version}').padding(top: 3.w).center(),
      ]
          .toColumn(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.max,
          )
          .padding(all: 8.w),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GetBuilder<HomeController>(
      init: HomeController(),
      id: "home",
      builder: (_) {
        return Scaffold(
          body: SafeArea(
            child: _buildView(),
          ),
        );
      },
    );
  }
}


/lib/home/controller.dart

import 'dart:async';

import 'package:calendar_view/calendar_view.dart';
import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:hive_flutter/hive_flutter.dart';

class HomeController extends GetxController with BaseControllerMixin {
  HomeController();

  @override
  String get builderId => 'home';

  static const String NOON_BREAK_START = 'NOON_BREAK_START';
  static const String NOON_BREAK_END = 'NOON_BREAK_END';
  static const String WORK_DAYS = 'WORK_DAYS';
  static const String WORK_HOURS = 'WORK_HOURS';

  final EventController eventController = EventController();
  final TextEditingController workDaysController = TextEditingController();
  final TextEditingController workHoursController = TextEditingController();

  late Box hiveBox;

  Timer? _clockTimer;

  RxString version = ''.obs;

  //当前时间
  RxString currentTime = ''.obs;
  //今日上班时间
  RxString todayStartTime = ''.obs;
  //今日下班时间
  RxString todayEndTime = ''.obs;

  //午休开始时间
  Rx<DateTime> noonBreakStart = Rx<DateTime>(DateTime.now());
  //午休结束时间
  Rx<DateTime> noonBreakEnd = Rx<DateTime>(DateTime.now());
  //午休时长
  RxString noonBreakDuration = ''.obs;

  //日历上显示的月份
  late DateTime showDate;

  //当月工作日天数
  late RxInt workDays = 0.obs;

  //每天工作时长
  late RxInt workHours = 0.obs;

  //当前工作总分钟数
  late RxInt countMinutes = 0.obs;

  @override
  void onInit() {
    super.onInit();
    showDate = DateTime.now();
    var start = getStringAsync(NOON_BREAK_START, defaultValue: '2000-10-10 11:30');
    var end = getStringAsync(NOON_BREAK_END, defaultValue: '2000-10-10 13:30');
    workDays.value = getIntAsync(WORK_DAYS, defaultValue: 21);
    workHours.value = getIntAsync(WORK_HOURS, defaultValue: 8);
    workDaysController.text = workDays.value.toString();
    workHoursController.text = workHours.value.toString();
    noonBreakStart.value = DateTime.parse(start);
    noonBreakEnd.value = DateTime.parse(end);
    updateNoonBreakDuration();
  }

  @override
  void onReady() async {
    super.onReady();
    version.value = await getVersion();
    hiveBox = await Hive.openBox('events');
    _saveTime();
    _runClockTimer();
    updateEvent();
  }

  void onPageChange(DateTime dateTime) {
    showDate = dateTime;
    updateEvent();
  }

  @override
  void onClose() {
    _clockTimer?.cancel();
    _clockTimer = null;
    eventController.dispose();
    workDaysController.dispose();
    workHoursController.dispose();
    super.onClose();
  }

  //时钟
  void _runClockTimer() {
    _clockTimer = Timer.periodic(
      const Duration(seconds: 1),
      (timer) {
        currentTime.value = getNowDateTimeString();
      },
    );
  }

  //记录当前时间
  void _saveTime() {
    String nowDate = DateTime.now().toDateString();
    String nowTime = DateTime.now().toTimeString();
    var data = hiveBox.get(nowDate, defaultValue: null);
    if (data == null) {
      todayStartTime.value = nowTime;
      hiveBox.put(
        nowDate,
        {
          'start': nowTime,
          'end': null,
        },
      );
    } else {
      todayStartTime.value = data['start'];
      todayEndTime.value = nowTime;
      data['end'] = nowTime;
      hiveBox.put(
        nowDate,
        data,
      );
    }
  }

  //获取指定日期的缓存
  Map? getEvent(DateTime date) {
    return hiveBox.get(date.toDateString(), defaultValue: null);
  }

  //更新当日上班时间
  void updateStartTime(DateTime time) {
    String nowDate = DateTime.now().toDateString();
    todayStartTime.value = time.toTimeString();
    var data = hiveBox.get(nowDate, defaultValue: null);
    data['start'] = time.toTimeString();
    hiveBox.put(
      nowDate,
      data,
    );
    updateEvent();
  }

  //更新下班时间
  void updateEndTime() {
    _saveTime();
    updateEvent();
  }

  //设置Event
  void updateEvent() async {
    // 获取当前月份的第一天
    DateTime firstDayOfMonth = DateTime(showDate.year, showDate.month, 1);
    // 获取下个月的第一天
    DateTime firstDayOfNextMonth = DateTime(showDate.year, showDate.month + 1, 1);
    // 计算当前月份的天数
    int daysInMonth = firstDayOfNextMonth.difference(firstDayOfMonth).inDays;
    // 创建一个列表,遍历每一天
    List<String> days = [];
    for (int i = 0; i < daysInMonth; i++) {
      days.add(firstDayOfMonth.add(Duration(days: i)).toDateString());
    }
    countMinutes.value = 0;
    // 从缓存中拿出全部数据
    for (String day in days) {
      Map? data = hiveBox.get(day, defaultValue: null);
      if (data != null) {
        final calendarEventData = CalendarEventData<Map>(
          title: day,
          date: DateTime.parse(day),
          event: data,
        );
        String? start = data['start'] as String?;
        String? end = data['end'] as String?;
        if (start != null && end != null) {
          countMinutes.value = countMinutes.value + getWorkDurationMinutes(start, end);
        }
        eventController.add(calendarEventData);
      }
    }
  }

  //获取午休时长
  Duration _getNoonBreakDuration() {
    return noonBreakEnd.value.difference(noonBreakStart.value);
  }

  //更新午休时长
  void updateNoonBreakDuration() {
    Duration difference = noonBreakEnd.value.difference(noonBreakStart.value);
    String hours = difference.inHours.toString().padLeft(2, '0');
    String minutes = (difference.inMinutes % 60).toString().padLeft(2, '0');
    String seconds = (difference.inSeconds % 60).toString().padLeft(2, '0');
    noonBreakDuration.value = '$hours:$minutes:$seconds';
  }

  //计算工时
  Duration getWorkDuration(String startTime, String endTime) {
    //午休开始时间
    DateTime noonStart = timeToDateTime(noonBreakStart.value.toTimeString());
    //午休结束时间
    DateTime noonEnd = timeToDateTime(noonBreakEnd.value.toTimeString());
    //将传入的Datetime调整为同一天
    DateTime start = timeToDateTime(startTime);
    DateTime end = timeToDateTime(endTime);
    //最终的计算时间
    DateTime computeStart;
    DateTime computeEnd;
    bool isNoonBreak = false;
    if ((start.isBefore(noonStart) && end.isBefore(noonStart)) ||
        (start.isAfter(noonEnd) && end.isAfter(noonEnd))) {
      //开始结束时间都在午休之前 或者 开始结束时间都在午休之后 最终的计算时间就是传入的时间
      computeStart = start;
      computeEnd = end;
    } else if (start.isBefore(noonStart) && end.isBefore(noonEnd)) {
      //开始时间在午休之前 结束时间在午休结束之前 结束时间将改为 午休开始时间
      computeStart = start;
      computeEnd = noonStart;
    } else if (start.isAfter(noonStart) && start.isBefore(noonEnd) && end.isAfter(noonEnd)) {
      //开始时间在午休之后 午休结束之前 结束时间在午休结束之后 开始时间将改为 午休结束时间
      computeStart = noonEnd;
      computeEnd = end;
    } else if (start.isBefore(noonStart) && end.isAfter(noonEnd)) {
      //开始时间在午休之前 结束时间在午休之后 正常情况 减去2小时午休
      computeStart = start;
      computeEnd = end;
      isNoonBreak = true;
    } else {
      //开始时间在午休开始之后 结束时间在午休结束之前 没有工时
      return const Duration(seconds: 0);
    }
    Duration difference = computeEnd.difference(computeStart);
    if (isNoonBreak) {
      difference = difference - _getNoonBreakDuration();
    }
    return difference;
  }

  //返回工时分钟数
  int getWorkDurationMinutes(String start, String end) {
    Duration duration = getWorkDuration(start, end);
    return duration.inMinutes;
  }

  //返回工时字符串
  String getWorkDurationString(String start, String end) {
    Duration duration = getWorkDuration(start, end);
    String hours = duration.inHours.toString().padLeft(2, '0');
    String minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
    String seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
    return '$hours:$minutes:$seconds';
  }

  //时间字符串转DateTime
  DateTime timeToDateTime(String? time) {
    time ??= '00:00:00';
    return DateTime.parse('2000-10-10 $time');
  }

  //补卡上班时间
  void startTimeRemedy(DateTime date, DateTime time) {
    String dateStr = date.toDateString();
    var data = hiveBox.get(dateStr, defaultValue: null);
    if (data == null) {
      hiveBox.put(
        dateStr,
        {
          'start': time.toTimeString(),
          'end': null,
        },
      );
    } else {
      data['start'] = time.toTimeString();
      hiveBox.put(
        dateStr,
        data,
      );
    }
    //更新日历
    updateEvent();
  }

  //补卡下班时间
  void endTimeRemedy(DateTime date, DateTime time) {
    String dateStr = date.toDateString();
    var data = hiveBox.get(dateStr, defaultValue: null);
    if (data == null) {
      showError('请先补卡上班时间');
    } else {
      data['end'] = time.toTimeString();
      hiveBox.put(
        dateStr,
        data,
      );
    }
    //更新日历
    updateEvent();
  }

  //清空指定日期的数据
  void cleanData(DateTime date) async {
    await hiveBox.delete(date.toDateString());
    final calendarEventData = eventController.getEventsOnDay(DateTime.parse(date.toDateString()));
    eventController.removeAll(calendarEventData);
    updateEvent();
  }

  //保存应上班天数
  void workDaysSave() {
    String text = workDaysController.text;
    int? number = int.tryParse(text);
    if (number == null) {
      showError('请输入有效的天数');
      return;
    }
    if (number < 1 || number > 31) {
      showError('请输入有效的天数');
      return;
    }
    workDays.value = number;
    setValue(WORK_DAYS, number);
  }

  //保存每日工时
  void workHoursSave() {
    String text = workHoursController.text;
    int? number = int.tryParse(text);
    if (number == null) {
      showError('请输入有效的时长');
      return;
    }
    if (number < 1 || number > 24) {
      showError('请输入有效的时长');
      return;
    }
    workHours.value = number;
    setValue(WORK_HOURS, number);
  }

  //正常工时
  int getWorkTimeMinutes() {
    return workDays.value * workHours.value * 60;
  }

  //加班时长
  int overtimeMinutes() {
    int minutes = getWorkTimeMinutes();
    if (countMinutes.value - minutes > 0) {
      return countMinutes.value - minutes;
    } else {
      return 0;
    }
  }
}