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/provider/health_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() async{
|
||||
void main() async {
|
||||
// 初始化Hive
|
||||
await Hive.initFlutter();
|
||||
|
||||
@@ -13,12 +14,12 @@ void main() async{
|
||||
Hive.registerAdapter(HealthRecordAdapter());
|
||||
|
||||
final healthsBox = await Hive.openBox<HealthRecord>('healths');
|
||||
healthsBox.clear();
|
||||
// await Hive.deleteBoxFromDisk('healths');
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => HealthProvider())
|
||||
],
|
||||
providers: [ChangeNotifierProvider(create: (_) => HealthProvider())],
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
@@ -31,6 +32,21 @@ class MyApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
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(
|
||||
fontFamily: 'Inter',
|
||||
scaffoldBackgroundColor: const Color(0xFFF9FAFB),
|
||||
|
||||
@@ -25,9 +25,6 @@ class HealthRecord extends HiveObject {
|
||||
@HiveField(5)
|
||||
String sleepEndTime;
|
||||
|
||||
@HiveField(6)
|
||||
num sleepDuration;
|
||||
|
||||
@HiveField(7)
|
||||
String date;
|
||||
|
||||
@@ -38,11 +35,10 @@ class HealthRecord extends HiveObject {
|
||||
required this.weight,
|
||||
required this.sleepStartTime,
|
||||
required this.sleepEndTime,
|
||||
required this.sleepDuration,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
static HealthRecord getEmpty() {
|
||||
static HealthRecord getEmpty(String date) {
|
||||
return HealthRecord(
|
||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
sportProject: '',
|
||||
@@ -50,21 +46,27 @@ class HealthRecord extends HiveObject {
|
||||
weight: 0,
|
||||
sleepStartTime: '',
|
||||
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(
|
||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
sportProject: '跑步',
|
||||
sportDuration: 30,
|
||||
weight: 70,
|
||||
sleepStartTime: '',
|
||||
sleepEndTime: '',
|
||||
sleepDuration: 0,
|
||||
date: '',
|
||||
id: id ?? this.id,
|
||||
date: date ?? this.date,
|
||||
weight: weight ?? this.weight,
|
||||
sportProject: sportProject ?? this.sportProject,
|
||||
sportDuration: sportDuration ?? this.sportDuration,
|
||||
sleepStartTime: sleepStartTime ?? this.sleepStartTime,
|
||||
sleepEndTime: sleepEndTime ?? this.sleepEndTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
||||
weight: fields[3] as num,
|
||||
sleepStartTime: fields[4] as String,
|
||||
sleepEndTime: fields[5] as String,
|
||||
sleepDuration: fields[6] as num,
|
||||
date: fields[7] as String,
|
||||
);
|
||||
}
|
||||
@@ -31,7 +30,7 @@ class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
||||
@override
|
||||
void write(BinaryWriter writer, HealthRecord obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -44,8 +43,6 @@ class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
||||
..write(obj.sleepStartTime)
|
||||
..writeByte(5)
|
||||
..write(obj.sleepEndTime)
|
||||
..writeByte(6)
|
||||
..write(obj.sleepDuration)
|
||||
..writeByte(7)
|
||||
..write(obj.date);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/flutter_common.dart';
|
||||
|
||||
class HealthProvider with ChangeNotifier {
|
||||
late HealthRecord _formItem;
|
||||
|
||||
HealthRecord get formItem => _formItem;
|
||||
|
||||
void resetForm() {
|
||||
_formItem = HealthRecord.getEmpty();
|
||||
notifyListeners();
|
||||
}
|
||||
late HealthRecord formItem;
|
||||
String date = formatDate(DateTime.now());
|
||||
RecordType type = RecordType.weight;
|
||||
bool isEditing = false;
|
||||
|
||||
void initForm(HealthRecord record) {
|
||||
_formItem = record;
|
||||
formItem = record;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateWeight(num weight) {
|
||||
_formItem.weight = weight;
|
||||
formItem.weight = weight;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSportProject(String sportProject) {
|
||||
_formItem.sportProject = sportProject;
|
||||
formItem.sportProject = sportProject;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSportDuration(num sportDuration) {
|
||||
_formItem.sportDuration = sportDuration;
|
||||
formItem.sportDuration = sportDuration;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSleepStartTime(String sleepStartTime) {
|
||||
_formItem.sleepStartTime = sleepStartTime;
|
||||
formItem.sleepStartTime = sleepStartTime;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSleepEndTime(String sleepEndTime) {
|
||||
_formItem.sleepEndTime = sleepEndTime;
|
||||
formItem.sleepEndTime = sleepEndTime;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,25 @@ class HealthService {
|
||||
}
|
||||
|
||||
HealthRecord getAllRecordByDate(String date) {
|
||||
return box.values.toList().firstWhere(
|
||||
final record = box.values.toList().firstWhere(
|
||||
(item) => item.date == date,
|
||||
orElse: () => HealthRecord.getEmpty(),
|
||||
orElse: () => HealthRecord.getEmpty(date),
|
||||
);
|
||||
|
||||
return record.copyWith();
|
||||
}
|
||||
|
||||
Future<bool> updateRecord(HealthRecord record) async {
|
||||
try {
|
||||
await record.save();
|
||||
List<HealthRecord> records =
|
||||
box.values.where((item) => item.date == record.date).toList();
|
||||
|
||||
if (records.isEmpty) {
|
||||
await addRecord(record);
|
||||
} else {
|
||||
await record.save();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
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: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