feat:增加睡眠记录
This commit is contained in:
@@ -2,10 +2,11 @@ import 'package:fitnote/models/health.dart';
|
|||||||
import 'package:fitnote/pages/home_page.dart';
|
import 'package:fitnote/pages/home_page.dart';
|
||||||
import 'package:fitnote/provider/health_provider.dart';
|
import 'package:fitnote/provider/health_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
void main() async{
|
void main() async {
|
||||||
// 初始化Hive
|
// 初始化Hive
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
|
|
||||||
@@ -13,12 +14,12 @@ void main() async{
|
|||||||
Hive.registerAdapter(HealthRecordAdapter());
|
Hive.registerAdapter(HealthRecordAdapter());
|
||||||
|
|
||||||
final healthsBox = await Hive.openBox<HealthRecord>('healths');
|
final healthsBox = await Hive.openBox<HealthRecord>('healths');
|
||||||
|
healthsBox.clear();
|
||||||
|
// await Hive.deleteBoxFromDisk('healths');
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [ChangeNotifierProvider(create: (_) => HealthProvider())],
|
||||||
ChangeNotifierProvider(create: (_) => HealthProvider())
|
|
||||||
],
|
|
||||||
child: MyApp(),
|
child: MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -31,6 +32,21 @@ class MyApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: '健康记录',
|
title: '健康记录',
|
||||||
|
localizationsDelegates: [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate
|
||||||
|
],
|
||||||
|
supportedLocales: [const Locale('zh'), const Locale('zh', 'CN')],
|
||||||
|
locale: Locale('zh', 'CN'),
|
||||||
|
builder: (context, child) {
|
||||||
|
return MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
alwaysUse24HourFormat: true,
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
scaffoldBackgroundColor: const Color(0xFFF9FAFB),
|
scaffoldBackgroundColor: const Color(0xFFF9FAFB),
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ class HealthRecord extends HiveObject {
|
|||||||
@HiveField(5)
|
@HiveField(5)
|
||||||
String sleepEndTime;
|
String sleepEndTime;
|
||||||
|
|
||||||
@HiveField(6)
|
|
||||||
num sleepDuration;
|
|
||||||
|
|
||||||
@HiveField(7)
|
@HiveField(7)
|
||||||
String date;
|
String date;
|
||||||
|
|
||||||
@@ -38,11 +35,10 @@ class HealthRecord extends HiveObject {
|
|||||||
required this.weight,
|
required this.weight,
|
||||||
required this.sleepStartTime,
|
required this.sleepStartTime,
|
||||||
required this.sleepEndTime,
|
required this.sleepEndTime,
|
||||||
required this.sleepDuration,
|
|
||||||
required this.date,
|
required this.date,
|
||||||
});
|
});
|
||||||
|
|
||||||
static HealthRecord getEmpty() {
|
static HealthRecord getEmpty(String date) {
|
||||||
return HealthRecord(
|
return HealthRecord(
|
||||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
sportProject: '',
|
sportProject: '',
|
||||||
@@ -50,21 +46,27 @@ class HealthRecord extends HiveObject {
|
|||||||
weight: 0,
|
weight: 0,
|
||||||
sleepStartTime: '',
|
sleepStartTime: '',
|
||||||
sleepEndTime: '',
|
sleepEndTime: '',
|
||||||
sleepDuration: 0,
|
date: date,
|
||||||
date: '',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static HealthRecord getDefault() {
|
HealthRecord copyWith({
|
||||||
|
int? id,
|
||||||
|
num? weight,
|
||||||
|
String? sportProject,
|
||||||
|
num? sportDuration,
|
||||||
|
String? sleepStartTime,
|
||||||
|
String? sleepEndTime,
|
||||||
|
String? date,
|
||||||
|
}) {
|
||||||
return HealthRecord(
|
return HealthRecord(
|
||||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
id: id ?? this.id,
|
||||||
sportProject: '跑步',
|
date: date ?? this.date,
|
||||||
sportDuration: 30,
|
weight: weight ?? this.weight,
|
||||||
weight: 70,
|
sportProject: sportProject ?? this.sportProject,
|
||||||
sleepStartTime: '',
|
sportDuration: sportDuration ?? this.sportDuration,
|
||||||
sleepEndTime: '',
|
sleepStartTime: sleepStartTime ?? this.sleepStartTime,
|
||||||
sleepDuration: 0,
|
sleepEndTime: sleepEndTime ?? this.sleepEndTime
|
||||||
date: '',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
|||||||
weight: fields[3] as num,
|
weight: fields[3] as num,
|
||||||
sleepStartTime: fields[4] as String,
|
sleepStartTime: fields[4] as String,
|
||||||
sleepEndTime: fields[5] as String,
|
sleepEndTime: fields[5] as String,
|
||||||
sleepDuration: fields[6] as num,
|
|
||||||
date: fields[7] as String,
|
date: fields[7] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -31,7 +30,7 @@ class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
|||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, HealthRecord obj) {
|
void write(BinaryWriter writer, HealthRecord obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(8)
|
..writeByte(7)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.id)
|
..write(obj.id)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -44,8 +43,6 @@ class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
|||||||
..write(obj.sleepStartTime)
|
..write(obj.sleepStartTime)
|
||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.sleepEndTime)
|
..write(obj.sleepEndTime)
|
||||||
..writeByte(6)
|
|
||||||
..write(obj.sleepDuration)
|
|
||||||
..writeByte(7)
|
..writeByte(7)
|
||||||
..write(obj.date);
|
..write(obj.date);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,40 @@
|
|||||||
import 'package:fitnote/models/health.dart';
|
import 'package:fitnote/models/health.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_common/flutter_common.dart';
|
||||||
|
|
||||||
class HealthProvider with ChangeNotifier {
|
class HealthProvider with ChangeNotifier {
|
||||||
late HealthRecord _formItem;
|
late HealthRecord formItem;
|
||||||
|
String date = formatDate(DateTime.now());
|
||||||
HealthRecord get formItem => _formItem;
|
RecordType type = RecordType.weight;
|
||||||
|
bool isEditing = false;
|
||||||
void resetForm() {
|
|
||||||
_formItem = HealthRecord.getEmpty();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initForm(HealthRecord record) {
|
void initForm(HealthRecord record) {
|
||||||
_formItem = record;
|
formItem = record;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateWeight(num weight) {
|
void updateWeight(num weight) {
|
||||||
_formItem.weight = weight;
|
formItem.weight = weight;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSportProject(String sportProject) {
|
void updateSportProject(String sportProject) {
|
||||||
_formItem.sportProject = sportProject;
|
formItem.sportProject = sportProject;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSportDuration(num sportDuration) {
|
void updateSportDuration(num sportDuration) {
|
||||||
_formItem.sportDuration = sportDuration;
|
formItem.sportDuration = sportDuration;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSleepStartTime(String sleepStartTime) {
|
void updateSleepStartTime(String sleepStartTime) {
|
||||||
_formItem.sleepStartTime = sleepStartTime;
|
formItem.sleepStartTime = sleepStartTime;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSleepEndTime(String sleepEndTime) {
|
void updateSleepEndTime(String sleepEndTime) {
|
||||||
_formItem.sleepEndTime = sleepEndTime;
|
formItem.sleepEndTime = sleepEndTime;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,25 @@ class HealthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HealthRecord getAllRecordByDate(String date) {
|
HealthRecord getAllRecordByDate(String date) {
|
||||||
return box.values.toList().firstWhere(
|
final record = box.values.toList().firstWhere(
|
||||||
(item) => item.date == date,
|
(item) => item.date == date,
|
||||||
orElse: () => HealthRecord.getEmpty(),
|
orElse: () => HealthRecord.getEmpty(date),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return record.copyWith();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> updateRecord(HealthRecord record) async {
|
Future<bool> updateRecord(HealthRecord record) async {
|
||||||
try {
|
try {
|
||||||
|
List<HealthRecord> records =
|
||||||
|
box.values.where((item) => item.date == record.date).toList();
|
||||||
|
|
||||||
|
if (records.isEmpty) {
|
||||||
|
await addRecord(record);
|
||||||
|
} else {
|
||||||
await record.save();
|
await record.save();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
55
lib/utils/record_utils.dart
Normal file
55
lib/utils/record_utils.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:fitnote/models/health.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
bool checkHasRecordValue(HealthRecord record, RecordType type) {
|
||||||
|
switch (type) {
|
||||||
|
case RecordType.weight:
|
||||||
|
return record.weight != 0;
|
||||||
|
case RecordType.sport:
|
||||||
|
return record.sportProject != '';
|
||||||
|
case RecordType.sleep:
|
||||||
|
return record.sleepStartTime != '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将字符串时间转换为 TimeOfDay
|
||||||
|
TimeOfDay parseTime(String timeString) {
|
||||||
|
if (timeString.isEmpty) {
|
||||||
|
return TimeOfDay.now();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final parts = timeString.split(':');
|
||||||
|
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
|
||||||
|
} catch (e) {
|
||||||
|
return TimeOfDay.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 TimeOfDay 格式化为字符串 (HH:mm)
|
||||||
|
String formatTime(TimeOfDay time) {
|
||||||
|
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算睡眠时长
|
||||||
|
String calculateDuration(String startTime, String endTime) {
|
||||||
|
try {
|
||||||
|
final start = parseTime(startTime);
|
||||||
|
final end = parseTime(endTime);
|
||||||
|
|
||||||
|
int startMinutes = start.hour * 60 + start.minute;
|
||||||
|
int endMinutes = end.hour * 60 + end.minute;
|
||||||
|
|
||||||
|
// 处理跨天情况(如果结束时间小于开始时间,认为是第二天)
|
||||||
|
if (endMinutes < startMinutes) {
|
||||||
|
endMinutes += 24 * 60; // 加上一天的分钟数
|
||||||
|
}
|
||||||
|
|
||||||
|
int durationMinutes = endMinutes - startMinutes;
|
||||||
|
int hours = durationMinutes ~/ 60;
|
||||||
|
int minutes = durationMinutes % 60;
|
||||||
|
|
||||||
|
return '${hours}小时${minutes}分钟';
|
||||||
|
} catch (e) {
|
||||||
|
return '计算错误';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:fitnote/widget/record/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getwidget/getwidget.dart';
|
import 'package:getwidget/getwidget.dart';
|
||||||
|
|
||||||
@@ -15,17 +16,15 @@ class DropdownSelect extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
final double width = 150;
|
||||||
|
final double height = 36;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 150,
|
width: width,
|
||||||
decoration: BoxDecoration(
|
decoration: buildBoxDecoration(context),
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: Border.all(color: colors.primary, width: 1.5),
|
|
||||||
),
|
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: GFDropdown(
|
child: GFDropdown(
|
||||||
itemHeight: 32,
|
itemHeight: height,
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
value: value,
|
value: value,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:fitnote/widget/record/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class NumberInput extends StatefulWidget {
|
class NumberInput extends StatefulWidget {
|
||||||
@@ -29,6 +30,8 @@ class _NumberInputState extends State<NumberInput> {
|
|||||||
late num _currentValue;
|
late num _currentValue;
|
||||||
|
|
||||||
final Color disabledColor = Colors.grey.shade400;
|
final Color disabledColor = Colors.grey.shade400;
|
||||||
|
final double width = 150;
|
||||||
|
final double height = 36;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -74,8 +77,8 @@ class _NumberInputState extends State<NumberInput> {
|
|||||||
final colors = Theme.of(context).colorScheme;
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 32,
|
width: height,
|
||||||
height: 32,
|
height: height,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: disabled ? disabledColor : colors.primary.withAlpha(100),
|
color: disabled ? disabledColor : colors.primary.withAlpha(100),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -89,14 +92,9 @@ class _NumberInputState extends State<NumberInput> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 150,
|
width: width,
|
||||||
decoration: BoxDecoration(
|
decoration: buildBoxDecoration(context),
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: Border.all(color: colors.primary, width: 1.5),
|
|
||||||
),
|
|
||||||
child: IntrinsicWidth(
|
child: IntrinsicWidth(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -35,3 +35,13 @@ Widget buildCircleIcon({
|
|||||||
child: Icon(icon, color: Theme.of(context).colorScheme.primary, size: 18),
|
child: Icon(icon, color: Theme.of(context).colorScheme.primary, size: 18),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoxDecoration buildBoxDecoration(BuildContext context) {
|
||||||
|
return BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:fitnote/models/health.dart';
|
import 'package:fitnote/models/health.dart';
|
||||||
import 'package:fitnote/provider/health_provider.dart';
|
import 'package:fitnote/provider/health_provider.dart';
|
||||||
import 'package:fitnote/service/health_service.dart';
|
import 'package:fitnote/service/health_service.dart';
|
||||||
|
import 'package:fitnote/utils/record_utils.dart';
|
||||||
import 'package:fitnote/widget/record/common.dart';
|
import 'package:fitnote/widget/record/common.dart';
|
||||||
import 'package:fitnote/widget/record/record_form.dart';
|
import 'package:fitnote/widget/record/record_form.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_common/widget/common_widget.dart';
|
import 'package:flutter_common/widget/common_widget.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:getwidget/getwidget.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -19,24 +21,18 @@ class RecordDaily extends StatefulWidget {
|
|||||||
class _RecordDailyState extends State<RecordDaily> {
|
class _RecordDailyState extends State<RecordDaily> {
|
||||||
final HealthService service = HealthService();
|
final HealthService service = HealthService();
|
||||||
|
|
||||||
late HealthRecord _record;
|
HealthRecord _record = HealthRecord.getEmpty('');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_record = service.getAllRecordByDate('2025-12-01');
|
|
||||||
}
|
|
||||||
|
|
||||||
final recordForm = HealthRecord(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
id: 1,
|
final provider = context.read<HealthProvider>();
|
||||||
sportProject: '羽毛球',
|
_record = service.getAllRecordByDate(provider.date);
|
||||||
sportDuration: 20,
|
provider.initForm(HealthRecord.getEmpty(provider.date));
|
||||||
sleepStartTime: '23:00',
|
});
|
||||||
sleepEndTime: '24:00',
|
}
|
||||||
sleepDuration: 60,
|
|
||||||
weight: 70,
|
|
||||||
date: '2025-11-28',
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -142,7 +138,7 @@ class _RecordDailyState extends State<RecordDaily> {
|
|||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_checkHasRecordValue(type)
|
checkHasRecordValue(_record, type)
|
||||||
? FontAwesomeIcons.pencil
|
? FontAwesomeIcons.pencil
|
||||||
: FontAwesomeIcons.plus,
|
: FontAwesomeIcons.plus,
|
||||||
color: type.color,
|
color: type.color,
|
||||||
@@ -159,29 +155,112 @@ class _RecordDailyState extends State<RecordDaily> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _checkHasRecordValue(RecordType type) {
|
|
||||||
switch (type) {
|
|
||||||
case RecordType.weight:
|
|
||||||
return _record.weight != 0;
|
|
||||||
case RecordType.sport:
|
|
||||||
return _record.sportProject != '';
|
|
||||||
case RecordType.sleep:
|
|
||||||
return _record.sleepStartTime != '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTapRecord(RecordType type) {
|
void onTapRecord(RecordType type) {
|
||||||
final provider = context.read<HealthProvider>();
|
final provider = context.read<HealthProvider>();
|
||||||
provider.initForm(HealthRecord.getDefault());
|
provider.isEditing = checkHasRecordValue(_record, type);
|
||||||
|
provider.type = type;
|
||||||
|
|
||||||
|
if (provider.isEditing) {
|
||||||
|
_initRecord(provider);
|
||||||
|
} else {
|
||||||
|
_defaultRecord(provider);
|
||||||
|
}
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Dialog(child: RecordForm(type: type)),
|
builder:
|
||||||
|
(context) => Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [RecordForm(type: type), _buildDialogButton()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDialogButton() {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GFButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_resetRecord();
|
||||||
|
},
|
||||||
|
color: colors.secondary,
|
||||||
|
text: "取消",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: GFButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_saveRecord();
|
||||||
|
},
|
||||||
|
color: colors.primary,
|
||||||
|
text: "确定",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initRecord(HealthProvider provider) {
|
||||||
|
switch (provider.type) {
|
||||||
|
case RecordType.weight:
|
||||||
|
provider.updateWeight(_record.weight);
|
||||||
|
break;
|
||||||
|
case RecordType.sport:
|
||||||
|
provider.updateSportProject(_record.sportProject);
|
||||||
|
provider.updateSportDuration(_record.sportDuration);
|
||||||
|
break;
|
||||||
|
case RecordType.sleep:
|
||||||
|
provider.updateSleepStartTime(_record.sleepStartTime);
|
||||||
|
provider.updateSleepEndTime(_record.sleepEndTime);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetRecord() {
|
||||||
|
final provider = context.read<HealthProvider>();
|
||||||
|
if (!provider.isEditing) {
|
||||||
|
_defaultRecord(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _defaultRecord(HealthProvider provider) {
|
||||||
|
switch (provider.type) {
|
||||||
|
case RecordType.weight:
|
||||||
|
provider.updateWeight(70);
|
||||||
|
break;
|
||||||
|
case RecordType.sport:
|
||||||
|
provider.updateSportProject('跑步');
|
||||||
|
provider.updateSportDuration(30);
|
||||||
|
break;
|
||||||
|
case RecordType.sleep:
|
||||||
|
provider.updateSleepStartTime('');
|
||||||
|
provider.updateSleepEndTime('');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveRecord() async {
|
||||||
|
final provider = context.read<HealthProvider>();
|
||||||
|
await service.updateRecord(provider.formItem);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_record = service.getAllRecordByDate(provider.date);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildRecordContent(RecordType type) {
|
Widget _buildRecordContent(RecordType type) {
|
||||||
if (!_checkHasRecordValue(type)) {
|
if (!checkHasRecordValue(_record, type)) {
|
||||||
return Text(
|
return Text(
|
||||||
"尚未记录今日数据,点击添加",
|
"尚未记录今日数据,点击添加",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||||
@@ -195,8 +274,16 @@ class _RecordDailyState extends State<RecordDaily> {
|
|||||||
case RecordType.sport:
|
case RecordType.sport:
|
||||||
title = "${_record.sportProject} · ${_record.sportDuration}分钟";
|
title = "${_record.sportProject} · ${_record.sportDuration}分钟";
|
||||||
case RecordType.sleep:
|
case RecordType.sleep:
|
||||||
title =
|
final startTime = _record.sleepStartTime;
|
||||||
"入睡 ${_record.sleepStartTime} · 起床 ${_record.sleepEndTime} · ${_record.sleepDuration}分钟";
|
final endTime = _record.sleepEndTime;
|
||||||
|
if (startTime.isNotEmpty && endTime.isNotEmpty) {
|
||||||
|
final duration = calculateDuration(startTime, endTime);
|
||||||
|
title = "入睡 $startTime · 起床 $endTime · ${duration}";
|
||||||
|
} else if (startTime.isNotEmpty) {
|
||||||
|
title = "入睡 $startTime";
|
||||||
|
} else if (endTime.isNotEmpty) {
|
||||||
|
title = "起床 $endTime";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:fitnote/models/health.dart';
|
import 'package:fitnote/models/health.dart';
|
||||||
import 'package:fitnote/provider/health_provider.dart';
|
import 'package:fitnote/provider/health_provider.dart';
|
||||||
import 'package:fitnote/service/health_service.dart';
|
import 'package:fitnote/service/health_service.dart';
|
||||||
|
import 'package:fitnote/utils/record_utils.dart';
|
||||||
import 'package:fitnote/widget/dropdown_select.dart';
|
import 'package:fitnote/widget/dropdown_select.dart';
|
||||||
import 'package:fitnote/widget/number_input.dart';
|
import 'package:fitnote/widget/number_input.dart';
|
||||||
|
import 'package:fitnote/widget/time_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:getwidget/getwidget.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class RecordForm extends StatefulWidget {
|
class RecordForm extends StatefulWidget {
|
||||||
@@ -21,40 +22,7 @@ class _RecordFormState extends State<RecordForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(padding: EdgeInsets.all(20), child: _buildFormContent());
|
return _buildFormContent();
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFormButton() {
|
|
||||||
final colors = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: GFButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
color: colors.secondary,
|
|
||||||
text: "取消",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: GFButton(
|
|
||||||
onPressed: () {
|
|
||||||
_saveRecord();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
color: colors.primary,
|
|
||||||
text: "确定",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _saveRecord() {
|
|
||||||
final provider = context.read<HealthProvider>();
|
|
||||||
|
|
||||||
// service.updateRecord(provider.formItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFormTitle(String title) {
|
Widget _buildFormTitle(String title) {
|
||||||
@@ -72,6 +40,34 @@ class _RecordFormState extends State<RecordForm> {
|
|||||||
return Text(title, style: TextStyle(fontSize: 16));
|
return Text(title, style: TextStyle(fontSize: 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSleepDuration(HealthRecord record) {
|
||||||
|
final startTime = record.sleepStartTime;
|
||||||
|
final endTime = record.sleepEndTime;
|
||||||
|
late String duration = calculateDuration(startTime, endTime);
|
||||||
|
|
||||||
|
if (startTime.isNotEmpty && endTime.isNotEmpty) {
|
||||||
|
duration = calculateDuration(startTime, endTime);
|
||||||
|
} else {
|
||||||
|
duration = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'睡眠时长:$duration',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFormContent() {
|
Widget _buildFormContent() {
|
||||||
final provider = context.watch<HealthProvider>();
|
final provider = context.watch<HealthProvider>();
|
||||||
|
|
||||||
@@ -98,7 +94,6 @@ class _RecordFormState extends State<RecordForm> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
_buildFormButton(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
case RecordType.sport:
|
case RecordType.sport:
|
||||||
@@ -120,7 +115,6 @@ class _RecordFormState extends State<RecordForm> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -137,18 +131,52 @@ class _RecordFormState extends State<RecordForm> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
_buildFormButton(),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
case RecordType.sleep:
|
case RecordType.sleep:
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildFiledTitle('记录睡眠'),
|
_buildFormTitle('记录睡眠'),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildFiledTitle('入睡时间:'),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
TimePicker(
|
||||||
|
initialTime: parseTime(provider.formItem.sleepStartTime),
|
||||||
|
label: provider.formItem.sleepStartTime,
|
||||||
|
onTimeSelected: (time) {
|
||||||
|
provider.updateSleepStartTime(formatTime(time));
|
||||||
|
final startTime = provider.formItem.sleepStartTime;
|
||||||
|
final endTime = provider.formItem.sleepEndTime;
|
||||||
|
calculateDuration(startTime, endTime);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
// 这里添加睡眠相关的输入控件
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildFiledTitle('起床时间:'),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
TimePicker(
|
||||||
|
initialTime: parseTime(provider.formItem.sleepEndTime),
|
||||||
|
label: provider.formItem.sleepEndTime,
|
||||||
|
onTimeSelected: (time) {
|
||||||
|
provider.updateSleepEndTime(formatTime(time));
|
||||||
|
final startTime = provider.formItem.sleepStartTime;
|
||||||
|
final endTime = provider.formItem.sleepEndTime;
|
||||||
|
calculateDuration(startTime, endTime);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
_buildFormButton(),
|
_buildSleepDuration(provider.formItem),
|
||||||
|
SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
69
lib/widget/time_picker.dart
Normal file
69
lib/widget/time_picker.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:fitnote/widget/record/common.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TimePicker extends StatelessWidget {
|
||||||
|
final TimeOfDay initialTime;
|
||||||
|
final ValueChanged<TimeOfDay> onTimeSelected;
|
||||||
|
final String? label;
|
||||||
|
|
||||||
|
const TimePicker({
|
||||||
|
super.key,
|
||||||
|
required this.initialTime,
|
||||||
|
required this.onTimeSelected,
|
||||||
|
this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double width = 150;
|
||||||
|
final double height = 36;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
decoration: buildBoxDecoration(context),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showTimePicker(context),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.access_time, size: 20, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
label ?? _formatTime(initialTime),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(Icons.arrow_drop_down, size: 20, color: Colors.grey[500]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showTimePicker(BuildContext context) async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: initialTime,
|
||||||
|
initialEntryMode: TimePickerEntryMode.dialOnly,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
onTimeSelected(picked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(TimeOfDay time) {
|
||||||
|
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -338,6 +338,11 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
name: fitnote
|
name: fitnote
|
||||||
Reference in New Issue
Block a user