feat:增加睡眠记录
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:fitnote/widget/record/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:getwidget/getwidget.dart';
|
||||
|
||||
@@ -15,17 +16,15 @@ class DropdownSelect extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final double width = 150;
|
||||
final double height = 36;
|
||||
|
||||
return Container(
|
||||
width: 150,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: colors.primary, width: 1.5),
|
||||
),
|
||||
width: width,
|
||||
decoration: buildBoxDecoration(context),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: GFDropdown(
|
||||
itemHeight: 32,
|
||||
itemHeight: height,
|
||||
padding: const EdgeInsets.all(0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
value: value,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fitnote/widget/record/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NumberInput extends StatefulWidget {
|
||||
@@ -29,6 +30,8 @@ class _NumberInputState extends State<NumberInput> {
|
||||
late num _currentValue;
|
||||
|
||||
final Color disabledColor = Colors.grey.shade400;
|
||||
final double width = 150;
|
||||
final double height = 36;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -74,8 +77,8 @@ class _NumberInputState extends State<NumberInput> {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: height,
|
||||
height: height,
|
||||
child: Material(
|
||||
color: disabled ? disabledColor : colors.primary.withAlpha(100),
|
||||
child: InkWell(
|
||||
@@ -89,14 +92,9 @@ class _NumberInputState extends State<NumberInput> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
width: 150,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: colors.primary, width: 1.5),
|
||||
),
|
||||
width: width,
|
||||
decoration: buildBoxDecoration(context),
|
||||
child: IntrinsicWidth(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -35,3 +35,13 @@ Widget buildCircleIcon({
|
||||
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/provider/health_provider.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/record_form.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:getwidget/getwidget.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -19,24 +21,18 @@ class RecordDaily extends StatefulWidget {
|
||||
class _RecordDailyState extends State<RecordDaily> {
|
||||
final HealthService service = HealthService();
|
||||
|
||||
late HealthRecord _record;
|
||||
HealthRecord _record = HealthRecord.getEmpty('');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_record = service.getAllRecordByDate('2025-12-01');
|
||||
}
|
||||
|
||||
final recordForm = HealthRecord(
|
||||
id: 1,
|
||||
sportProject: '羽毛球',
|
||||
sportDuration: 20,
|
||||
sleepStartTime: '23:00',
|
||||
sleepEndTime: '24:00',
|
||||
sleepDuration: 60,
|
||||
weight: 70,
|
||||
date: '2025-11-28',
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = context.read<HealthProvider>();
|
||||
_record = service.getAllRecordByDate(provider.date);
|
||||
provider.initForm(HealthRecord.getEmpty(provider.date));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -142,7 +138,7 @@ class _RecordDailyState extends State<RecordDaily> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
_checkHasRecordValue(type)
|
||||
checkHasRecordValue(_record, type)
|
||||
? FontAwesomeIcons.pencil
|
||||
: FontAwesomeIcons.plus,
|
||||
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) {
|
||||
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(
|
||||
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) {
|
||||
if (!_checkHasRecordValue(type)) {
|
||||
if (!checkHasRecordValue(_record, type)) {
|
||||
return Text(
|
||||
"尚未记录今日数据,点击添加",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
@@ -195,8 +274,16 @@ class _RecordDailyState extends State<RecordDaily> {
|
||||
case RecordType.sport:
|
||||
title = "${_record.sportProject} · ${_record.sportDuration}分钟";
|
||||
case RecordType.sleep:
|
||||
title =
|
||||
"入睡 ${_record.sleepStartTime} · 起床 ${_record.sleepEndTime} · ${_record.sleepDuration}分钟";
|
||||
final startTime = _record.sleepStartTime;
|
||||
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(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:fitnote/provider/health_provider.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/number_input.dart';
|
||||
import 'package:fitnote/widget/time_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:getwidget/getwidget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecordForm extends StatefulWidget {
|
||||
@@ -21,40 +22,7 @@ class _RecordFormState extends State<RecordForm> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(padding: EdgeInsets.all(20), child: _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);
|
||||
return _buildFormContent();
|
||||
}
|
||||
|
||||
Widget _buildFormTitle(String title) {
|
||||
@@ -72,6 +40,34 @@ class _RecordFormState extends State<RecordForm> {
|
||||
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() {
|
||||
final provider = context.watch<HealthProvider>();
|
||||
|
||||
@@ -98,7 +94,6 @@ class _RecordFormState extends State<RecordForm> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
_buildFormButton(),
|
||||
],
|
||||
);
|
||||
case RecordType.sport:
|
||||
@@ -120,7 +115,6 @@ class _RecordFormState extends State<RecordForm> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -137,18 +131,52 @@ class _RecordFormState extends State<RecordForm> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
_buildFormButton(),
|
||||
],
|
||||
);
|
||||
case RecordType.sleep:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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),
|
||||
// 这里添加睡眠相关的输入控件
|
||||
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),
|
||||
_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')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user