公司最近改为了弹性考勤制度,不含午休时间一天要干8小时,多出的工时每4小时算半天加班。考虑到苍蝇腿也是肉,避免3.9小时的尴尬,使用GetxScaffold框架花1天时间写了这个打工人工时计算工具。也算是给使用GetxScaffold脚手架的同学做个示例,有不同需求的同学也可以根据自己的需求自行更改。
关于GetxScaffold
GetXScaffold 快速开发脚手架在 GetX 框架和一些常用插件的基础上,构建了一套完整的快速开发模板。其中包括新增了部分常用功能的全局方法、常用的扩展方法和各种工具类、部分常用组件的封装、简单易用的对话框、二次封装的 Dio 网络请求工具、二次封装的 GetxController、二次封装的应用主题和国际化实现等。GetXScaffold 是对以上这些内容的过度封装,包括一些组件的扩展方法会违背 Flutter 本身的开发规范,改变你的开发习惯。所以本脚手架单纯为了提高开发效率,减少重复代码,减少开发成本。如果您是刚接触 Flutter 开发并还处在学习过程中的话,并不推荐您使用该脚手架。以下只是部分功能的使用示例,建议您通过示例项目或者源码了解全部使用方法。
关于WorkHelper
应用每天首次打开则记录当前时间为上班时间,再次打开或者点击右上方按钮则更新下班时间。左侧日历则统计每日工时。可根据自己情况设置午休时间,每月应上班的天数和每日的工时,下方做相关统计。纯纯牛马!!!
项目依赖
/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;
}
}
}