feat:增加数据存储
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import 'package:fitnote/modesl/menu.dart';
|
||||
import 'package:fitnote/models/menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppNavbar extends StatefulWidget {
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
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:hive_flutter/hive_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const HealthLogApp());
|
||||
void main() async{
|
||||
// 初始化Hive
|
||||
await Hive.initFlutter();
|
||||
|
||||
// 注册适配器
|
||||
Hive.registerAdapter(HealthRecordAdapter());
|
||||
|
||||
final healthsBox = await Hive.openBox<HealthRecord>('healths');
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => HealthProvider())
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class HealthLogApp extends StatelessWidget {
|
||||
const HealthLogApp({super.key});
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
82
lib/models/health.dart
Normal file
82
lib/models/health.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
// flutter packages pub run build_runner build
|
||||
part 'health.g.dart';
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
class HealthRecord extends HiveObject {
|
||||
@HiveField(0)
|
||||
int id;
|
||||
|
||||
@HiveField(1)
|
||||
String sportProject;
|
||||
|
||||
@HiveField(2)
|
||||
num sportDuration;
|
||||
|
||||
@HiveField(3)
|
||||
num weight;
|
||||
|
||||
@HiveField(4)
|
||||
String sleepStartTime;
|
||||
|
||||
@HiveField(5)
|
||||
String sleepEndTime;
|
||||
|
||||
@HiveField(6)
|
||||
num sleepDuration;
|
||||
|
||||
@HiveField(7)
|
||||
String date;
|
||||
|
||||
HealthRecord({
|
||||
required this.id,
|
||||
required this.sportProject,
|
||||
required this.sportDuration,
|
||||
required this.weight,
|
||||
required this.sleepStartTime,
|
||||
required this.sleepEndTime,
|
||||
required this.sleepDuration,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
static HealthRecord getEmpty() {
|
||||
return HealthRecord(
|
||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
sportProject: '',
|
||||
sportDuration: 0,
|
||||
weight: 0,
|
||||
sleepStartTime: '',
|
||||
sleepEndTime: '',
|
||||
sleepDuration: 0,
|
||||
date: '',
|
||||
);
|
||||
}
|
||||
|
||||
static HealthRecord getDefault() {
|
||||
return HealthRecord(
|
||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
sportProject: '跑步',
|
||||
sportDuration: 30,
|
||||
weight: 70,
|
||||
sleepStartTime: '',
|
||||
sleepEndTime: '',
|
||||
sleepDuration: 0,
|
||||
date: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum RecordType {
|
||||
weight('体重', FontAwesomeIcons.weightScale, Color(0xFFF59E0B)),
|
||||
sport('运动', FontAwesomeIcons.personRunning, Color(0xFF38BDF8)),
|
||||
sleep('睡眠', FontAwesomeIcons.moon, Color(0xFF10B981));
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const RecordType(this.title, this.icon, this.color);
|
||||
}
|
||||
62
lib/models/health.g.dart
Normal file
62
lib/models/health.g.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'health.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class HealthRecordAdapter extends TypeAdapter<HealthRecord> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
HealthRecord read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return HealthRecord(
|
||||
id: fields[0] as int,
|
||||
sportProject: fields[1] as String,
|
||||
sportDuration: fields[2] as num,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, HealthRecord obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.sportProject)
|
||||
..writeByte(2)
|
||||
..write(obj.sportDuration)
|
||||
..writeByte(3)
|
||||
..write(obj.weight)
|
||||
..writeByte(4)
|
||||
..write(obj.sleepStartTime)
|
||||
..writeByte(5)
|
||||
..write(obj.sleepEndTime)
|
||||
..writeByte(6)
|
||||
..write(obj.sleepDuration)
|
||||
..writeByte(7)
|
||||
..write(obj.date);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is HealthRecordAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class HealthRecord {
|
||||
@JsonKey(name: 'id')
|
||||
int? id;
|
||||
|
||||
@JsonKey(name: 'sportProject')
|
||||
String? sportProject;
|
||||
|
||||
@JsonKey(name: 'sportDuration')
|
||||
num? sportDuration;
|
||||
|
||||
@JsonKey(name: 'weight')
|
||||
num? weight;
|
||||
|
||||
@JsonKey(name: 'sleepStartTime')
|
||||
String? sleepStartTime;
|
||||
|
||||
@JsonKey(name: 'sleepEndTime')
|
||||
String? sleepEndTime;
|
||||
|
||||
@JsonKey(name: 'sleepDuration')
|
||||
num? sleepDuration;
|
||||
|
||||
@JsonKey(name: 'date')
|
||||
String date;
|
||||
|
||||
HealthRecord({
|
||||
this.id,
|
||||
this.sportProject,
|
||||
this.sportDuration,
|
||||
this.weight,
|
||||
this.sleepStartTime,
|
||||
this.sleepEndTime,
|
||||
this.sleepDuration,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
// factory Health.fromJson(Map<String, dynamic> json) => _$HealthFromJson(json);
|
||||
//
|
||||
// Map<String, dynamic> toJson() => _$HealthToJson(this);
|
||||
}
|
||||
|
||||
enum RecordType {
|
||||
weight('体重', FontAwesomeIcons.weightScale, Color(0xFFF59E0B)),
|
||||
sport('运动', FontAwesomeIcons.personRunning, Color(0xFF38BDF8)),
|
||||
sleep('睡眠', FontAwesomeIcons.moon, Color(0xFF10B981));
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const RecordType(this.title, this.icon, this.color);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:fitnote/layout/app_navbar.dart';
|
||||
import 'package:fitnote/modesl/menu.dart';
|
||||
import 'package:fitnote/models/menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:fitnote/widget/record/goal_progress.dart';
|
||||
import 'package:fitnote/widget/record/today_record.dart';
|
||||
import 'package:fitnote/widget/record/record_daily.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -24,12 +24,13 @@ class _RecordPageState extends State<RecordPage> {
|
||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
return weekdays[weekday - 1];
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [GoalProgress(), const SizedBox(height: 24), TodayRecord()],
|
||||
children: [GoalProgress(), const SizedBox(height: 24), RecordDaily()],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
43
lib/provider/health_provider.dart
Normal file
43
lib/provider/health_provider.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HealthProvider with ChangeNotifier {
|
||||
late HealthRecord _formItem;
|
||||
|
||||
HealthRecord get formItem => _formItem;
|
||||
|
||||
void resetForm() {
|
||||
_formItem = HealthRecord.getEmpty();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void initForm(HealthRecord record) {
|
||||
_formItem = record;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateWeight(num weight) {
|
||||
_formItem.weight = weight;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSportProject(String sportProject) {
|
||||
_formItem.sportProject = sportProject;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSportDuration(num sportDuration) {
|
||||
_formItem.sportDuration = sportDuration;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSleepStartTime(String sleepStartTime) {
|
||||
_formItem.sleepStartTime = sleepStartTime;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSleepEndTime(String sleepEndTime) {
|
||||
_formItem.sleepEndTime = sleepEndTime;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
46
lib/service/health_service.dart
Normal file
46
lib/service/health_service.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
class HealthService {
|
||||
static const String boxName = 'healths';
|
||||
|
||||
Box<HealthRecord> get box => Hive.box<HealthRecord>(boxName);
|
||||
|
||||
Future<bool> addRecord(HealthRecord record) async {
|
||||
try {
|
||||
await box.add(record);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<HealthRecord> getAllRecords() {
|
||||
return box.values.toList();
|
||||
}
|
||||
|
||||
HealthRecord getAllRecordByDate(String date) {
|
||||
return box.values.toList().firstWhere(
|
||||
(item) => item.date == date,
|
||||
orElse: () => HealthRecord.getEmpty(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> updateRecord(HealthRecord record) async {
|
||||
try {
|
||||
await record.save();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteRecord(HealthRecord record) async {
|
||||
try {
|
||||
await record.delete();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:fitnote/modesl/health.dart';
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:fitnote/widget/record/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_common/widget/common_widget.dart';
|
||||
|
||||
211
lib/widget/record/record_daily.dart
Normal file
211
lib/widget/record/record_daily.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:fitnote/provider/health_provider.dart';
|
||||
import 'package:fitnote/service/health_service.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:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecordDaily extends StatefulWidget {
|
||||
const RecordDaily({super.key});
|
||||
|
||||
@override
|
||||
State<RecordDaily> createState() => _RecordDailyState();
|
||||
}
|
||||
|
||||
class _RecordDailyState extends State<RecordDaily> {
|
||||
final HealthService service = HealthService();
|
||||
|
||||
late HealthRecord _record;
|
||||
|
||||
@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',
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
buildCardTitle(
|
||||
context: context,
|
||||
title: '今日记录',
|
||||
icon: FontAwesomeIcons.calendarCheck,
|
||||
),
|
||||
_buildTodayTag(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: RecordType.values.length,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final recordType = RecordType.values[index];
|
||||
return _buildRecordListTile(recordType);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodayTag() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
DateFormat('MM月dd日').format(DateTime.now()),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordListTile(RecordType type) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [type.color.withAlpha(10), Colors.white.withAlpha(200)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(10),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: type.color.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(type.icon, color: type.color, size: 22),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
type.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: type.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildRecordContent(type),
|
||||
],
|
||||
),
|
||||
trailing: GestureDetector(
|
||||
onTap: () => onTapRecord(type),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: type.color.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
_checkHasRecordValue(type)
|
||||
? FontAwesomeIcons.pencil
|
||||
: FontAwesomeIcons.plus,
|
||||
color: type.color,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(child: RecordForm(type: type)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordContent(RecordType type) {
|
||||
if (!_checkHasRecordValue(type)) {
|
||||
return Text(
|
||||
"尚未记录今日数据,点击添加",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
);
|
||||
}
|
||||
|
||||
late String title;
|
||||
switch (type) {
|
||||
case RecordType.weight:
|
||||
title = "${_record.weight} kg";
|
||||
case RecordType.sport:
|
||||
title = "${_record.sportProject} · ${_record.sportDuration}分钟";
|
||||
case RecordType.sleep:
|
||||
title =
|
||||
"入睡 ${_record.sleepStartTime} · 起床 ${_record.sleepEndTime} · ${_record.sleepDuration}分钟";
|
||||
}
|
||||
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
156
lib/widget/record/record_form.dart
Normal file
156
lib/widget/record/record_form.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:fitnote/models/health.dart';
|
||||
import 'package:fitnote/provider/health_provider.dart';
|
||||
import 'package:fitnote/service/health_service.dart';
|
||||
import 'package:fitnote/widget/dropdown_select.dart';
|
||||
import 'package:fitnote/widget/number_input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:getwidget/getwidget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecordForm extends StatefulWidget {
|
||||
final RecordType type;
|
||||
|
||||
const RecordForm({super.key, required this.type});
|
||||
|
||||
@override
|
||||
State<RecordForm> createState() => _RecordFormState();
|
||||
}
|
||||
|
||||
class _RecordFormState extends State<RecordForm> {
|
||||
final HealthService service = HealthService();
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
Widget _buildFormTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFiledTitle(String title) {
|
||||
return Text(title, style: TextStyle(fontSize: 16));
|
||||
}
|
||||
|
||||
Widget _buildFormContent() {
|
||||
final provider = context.watch<HealthProvider>();
|
||||
|
||||
switch (widget.type) {
|
||||
case RecordType.weight:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildFormTitle('记录体重'),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFiledTitle('体重:'),
|
||||
SizedBox(width: 10),
|
||||
NumberInput(
|
||||
value: provider.formItem.weight,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.1,
|
||||
unit: 'Kg',
|
||||
onChanged: (value) => provider.updateWeight(value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
_buildFormButton(),
|
||||
],
|
||||
);
|
||||
case RecordType.sport:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildFormTitle('记录运动'),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFiledTitle('运动类型:'),
|
||||
SizedBox(width: 10),
|
||||
DropdownSelect(
|
||||
value: provider.formItem.sportProject,
|
||||
items: ['羽毛球', '乒乓球', '篮球', '跑步'],
|
||||
onChanged: (value) => provider.updateSportProject(value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFiledTitle('运动时长:'),
|
||||
SizedBox(width: 10),
|
||||
NumberInput(
|
||||
value: provider.formItem.sportDuration,
|
||||
min: 5,
|
||||
max: 240,
|
||||
step: 5,
|
||||
unit: '分钟',
|
||||
onChanged: (value) => provider.updateSportDuration(value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
_buildFormButton(),
|
||||
],
|
||||
);
|
||||
case RecordType.sleep:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildFiledTitle('记录睡眠'),
|
||||
SizedBox(height: 20),
|
||||
// 这里添加睡眠相关的输入控件
|
||||
SizedBox(height: 20),
|
||||
_buildFormButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
import 'package:fitnote/modesl/health.dart';
|
||||
import 'package:fitnote/widget/dropdown_select.dart';
|
||||
import 'package:fitnote/widget/number_input.dart';
|
||||
import 'package:fitnote/widget/record/common.dart';
|
||||
import 'package:fitnote/widget/select_input.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:numberpicker/numberpicker.dart';
|
||||
|
||||
class TodayRecord extends StatefulWidget {
|
||||
const TodayRecord({super.key});
|
||||
|
||||
@override
|
||||
State<TodayRecord> createState() => _TodayRecordState();
|
||||
}
|
||||
|
||||
class _TodayRecordState extends State<TodayRecord> {
|
||||
final record = HealthRecord(
|
||||
id: 1,
|
||||
sportProject: '羽毛球',
|
||||
sportDuration: 20,
|
||||
sleepStartTime: '23:00',
|
||||
sleepEndTime: '24:00',
|
||||
sleepDuration: 60,
|
||||
weight: 0,
|
||||
date: '2025-11-28',
|
||||
);
|
||||
|
||||
final recordForm = HealthRecord(
|
||||
id: 1,
|
||||
sportProject: '羽毛球',
|
||||
sportDuration: 20,
|
||||
sleepStartTime: '23:00',
|
||||
sleepEndTime: '24:00',
|
||||
sleepDuration: 60,
|
||||
weight: 70,
|
||||
date: '2025-11-28',
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
buildCardTitle(
|
||||
context: context,
|
||||
title: '今日记录',
|
||||
icon: FontAwesomeIcons.calendarCheck,
|
||||
),
|
||||
_buildTodayTag(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: RecordType.values.length,
|
||||
separatorBuilder: (context, index) => SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final recordType = RecordType.values[index];
|
||||
return _buildRecordListTile(recordType);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodayTag() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
DateFormat('MM月dd日').format(DateTime.now()),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordListTile(RecordType type) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [type.color.withAlpha(10), Colors.white.withAlpha(200)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(10),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: type.color.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(type.icon, color: type.color, size: 22),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
type.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: type.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildRecordContent(type),
|
||||
],
|
||||
),
|
||||
trailing: GestureDetector(
|
||||
onTap: () => onTapRecord(type),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: type.color.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
_checkHasRecordValue(type)
|
||||
? FontAwesomeIcons.pencil
|
||||
: FontAwesomeIcons.plus,
|
||||
color: type.color,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _checkHasRecordValue(RecordType type) {
|
||||
switch (type) {
|
||||
case RecordType.weight:
|
||||
return record.weight != null;
|
||||
case RecordType.sport:
|
||||
return record.sportProject != null;
|
||||
case RecordType.sleep:
|
||||
return record.sleepStartTime != null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDialogButton(RecordType type) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GFButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
color: colors.secondary.withAlpha(100),
|
||||
text: "取消",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GFButton(onPressed: () {}, color: colors.primary, text: "确定"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// void _saveRecordData(RecordType type) {
|
||||
// setState(() {
|
||||
// switch (type) {
|
||||
// case RecordType.weight:
|
||||
// record.weight = _dialogWeight;
|
||||
// break;
|
||||
// case RecordType.sport:
|
||||
// record.sportProject = _dialogSportProject;
|
||||
// record.sportDuration = _dialogSportDuration.toInt();
|
||||
// break;
|
||||
// case RecordType.sleep:
|
||||
// // 处理睡眠数据
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
Widget _buildDialogTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFiledTitle(String title) {
|
||||
return Text(title, style: TextStyle(fontSize: 16));
|
||||
}
|
||||
|
||||
Widget _buildDialogContent(RecordType type) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
|
||||
switch (type) {
|
||||
case RecordType.weight:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDialogTitle('记录体重'),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFiledTitle('体重:'),
|
||||
SizedBox(width: 10),
|
||||
NumberInput(
|
||||
value: recordForm.weight!,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.1,
|
||||
unit: 'Kg',
|
||||
onChanged:
|
||||
(value) => setState(() => recordForm.weight = value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
_buildDialogButton(type),
|
||||
],
|
||||
);
|
||||
case RecordType.sport:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDialogTitle('记录运动'),
|
||||
SizedBox(height: 24),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFiledTitle('运动类型:'),
|
||||
SizedBox(width: 10),
|
||||
DropdownSelect(
|
||||
value: recordForm.sportProject!,
|
||||
items: ['羽毛球', '乒乓球', '篮球', '跑步'],
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
recordForm.sportProject = newValue!;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 运动时长
|
||||
_buildFiledTitle('运动时长:'),
|
||||
SizedBox(width: 10),
|
||||
NumberInput(
|
||||
value: recordForm.sportDuration,
|
||||
min: 5,
|
||||
max: 240,
|
||||
step: 5,
|
||||
unit: '分钟',
|
||||
onChanged:
|
||||
(value) =>
|
||||
setState(() => recordForm.sportDuration = value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
_buildDialogButton(type),
|
||||
],
|
||||
);
|
||||
case RecordType.sleep:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildFiledTitle('记录睡眠'),
|
||||
SizedBox(height: 20),
|
||||
// 这里添加睡眠相关的输入控件
|
||||
SizedBox(height: 20),
|
||||
_buildDialogButton(type),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onTapRecord(RecordType type) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => Dialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: _buildDialogContent(type),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordContent(RecordType type) {
|
||||
if (!_checkHasRecordValue(type)) {
|
||||
return Text(
|
||||
"尚未记录今日数据,点击添加",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
);
|
||||
}
|
||||
|
||||
late String title;
|
||||
switch (type) {
|
||||
case RecordType.weight:
|
||||
title = "${record.weight} kg";
|
||||
case RecordType.sport:
|
||||
title = "${record.sportProject} · ${record.sportDuration}分钟";
|
||||
case RecordType.sleep:
|
||||
title =
|
||||
"入睡 ${record.sleepStartTime} · 起床 ${record.sleepEndTime} · ${record.sleepDuration}分钟";
|
||||
}
|
||||
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
pubspec.lock
69
pubspec.lock
@@ -5,18 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "85.0.0"
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.7.1"
|
||||
version: "6.11.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -197,10 +202,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "2.3.8"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -399,6 +404,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
hive_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive_flutter
|
||||
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
hive_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: hive_generator
|
||||
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -456,21 +485,13 @@ packages:
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.9.5"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -519,6 +540,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -672,7 +701,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
@@ -792,18 +821,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "1.5.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.7"
|
||||
version: "1.3.5"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -1 +1 @@
|
||||
name: fitnote
|
||||
name: fitnote
|
||||
Reference in New Issue
Block a user