feat:增加TDesign组件库

This commit is contained in:
2026-01-05 15:33:43 +08:00
parent 2b334edf2f
commit 00c8e82a65
18 changed files with 364 additions and 304 deletions

View File

@@ -1,157 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:intl/intl.dart';
TextStyle _titleStyle() {
return TextStyle(fontSize: 16, fontWeight: FontWeight.w600);
}
TextStyle _contentStyle() {
return TextStyle(fontSize: 16, fontWeight: FontWeight.w500);
}
OutlineInputBorder _border(ColorScheme colors) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.onSurface.withAlpha(100), width: 1.5),
);
}
OutlineInputBorder _enabledBorder(ColorScheme colors) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.onSurface.withAlpha(100), width: 1.5),
);
}
OutlineInputBorder _focusedBorder(ColorScheme colors) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary, width: 2),
);
}
EdgeInsetsGeometry _containerPadding() {
return EdgeInsets.symmetric(horizontal: 16, vertical: 10);
}
Widget buildFormTime({
required BuildContext context,
required String name,
required String title,
required DateTime? initialValue,
required ValueChanged<DateTime?> onChanged,
double width = 100,
String hintText = '请选择时间',
}) {
final colors = Theme.of(context).colorScheme;
final alphaColor = colors.onSurface.withAlpha(100);
return Row(
children: [
Container(width: width, child: Text(title, style: _titleStyle())),
const SizedBox(width: 8),
Expanded(
child: FormBuilderDateTimePicker(
name: name,
inputType: InputType.time,
initialTime: TimeOfDay.now(),
initialValue: initialValue,
format: DateFormat.Hm(),
onChanged: (value) => onChanged(value),
style: _contentStyle(),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: alphaColor),
prefixIcon: Icon(Icons.schedule_rounded, size: 24),
border: _border(colors),
enabledBorder: _enabledBorder(colors),
focusedBorder: _focusedBorder(colors),
filled: true,
fillColor: colors.surfaceContainer,
contentPadding: _containerPadding(),
),
),
),
],
);
}
Widget buildFormSlider({
required BuildContext context,
required String name,
required String title,
required String label,
required double min,
required double max,
required double initialValue,
required int divisions,
required ValueChanged<double?> onChanged,
double width = 100,
}) {
final colors = Theme.of(context).colorScheme;
final alphaColor = colors.onSurface.withAlpha(100);
return Row(
children: [
Container(width: width, child: Text(title, style: _titleStyle())),
const SizedBox(width: 8),
Expanded(
child: FormBuilderSlider(
name: name,
min: min,
max: max,
initialValue: initialValue,
divisions: divisions,
onChanged: (value) => onChanged(value),
label: label,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
],
);
}
Widget buildFormRadioGroup({
required BuildContext context,
required String name,
required String title,
required String initialValue,
required List<String> items,
required ValueChanged<String?> onChanged,
double width = 100,
}) {
final colors = Theme.of(context).colorScheme;
final alphaColor = colors.onSurface.withAlpha(100);
return Row(
children: [
Container(width: width, child: Text(title, style: _titleStyle())),
const SizedBox(width: 8),
Expanded(
child: FormBuilderRadioGroup<String>(
name: name,
options:
items
.map(
(item) =>
FormBuilderFieldOption(value: item, child: Text(item)),
)
.toList(),
initialValue: initialValue,
activeColor: colors.primary,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
onChanged: (value) => onChanged(value),
),
),
],
);
}

View File

@@ -1,6 +1,5 @@
import 'package:fitnote/utils/record_utils.dart';
import 'package:flutter/material.dart';
import 'package:getwidget/getwidget.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
Widget buildCardTitle({
required BuildContext context,
@@ -70,17 +69,14 @@ Widget buildProgressItem({
],
),
const SizedBox(height: 8),
GFProgressBar(
animation: true,
lineHeight: 20,
percentage: progress,
progressBarColor: color,
child: Text(
'${roundNum(progress * 100)}%',
textAlign: TextAlign.end,
style: TextStyle(color: Colors.white),
),
),
TDProgress(
type: TDProgressType.linear,
progressStatus: TDProgressStatus.primary,
value: progress,
color: color,
strokeWidth: 20,
progressLabelPosition: TDProgressLabelPosition.inside,
)
],
);
}

View File

@@ -51,7 +51,7 @@ class _GoalProgressState extends State<GoalProgress> {
const SizedBox(height: 28),
buildProgressItem(
title: '目标体重',
current: provider.latestWeight.toString(),
current: provider.latestWeight.toStringAsFixed(1),
target: AppConfig.goalWeight.toString(),
unit: RecordType.weight.unit,
progress: calWeightProgress(

View File

@@ -9,9 +9,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_common/flutter_common.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';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecordDaily extends StatefulWidget {
const RecordDaily({super.key});
@@ -172,7 +172,7 @@ class _RecordDailyState extends State<RecordDaily> {
break;
case RecordType.sport:
provider.updateSportProject(AppConfig.sportProjectList[0]);
provider.updateSportDuration(30);
provider.updateSportDuration(AppConfig.initSpot);
break;
case RecordType.sleep:
provider.updateSleepStartTime('');
@@ -202,64 +202,44 @@ class _RecordDailyState extends State<RecordDaily> {
return Row(
children: [
Expanded(
child: GFButton(
onPressed: () {
child: TDButton(
text: '取消',
theme: TDButtonTheme.light,
onTap: () {
Navigator.of(context).pop();
_resetRecord();
},
color: Colors.grey,
text: "取消",
),
),
const SizedBox(width: 12),
Expanded(
child: GFButton(
onPressed: () {
child: TDButton(
text: '删除',
theme: TDButtonTheme.danger,
onTap: () {
Navigator.of(context).pop();
_deleteRecord(type);
},
color: Colors.red,
text: "删除",
),
),
const SizedBox(width: 12),
Expanded(
child: GFButton(
onPressed: () {
child: TDButton(
text: '确定',
theme: TDButtonTheme.primary,
onTap: () {
Navigator.of(context).pop();
_saveRecord();
},
color: colors.primary,
text: "确定",
),
),
],
);
}
void _resetRecord() {
final provider = context.read<HealthProvider>();
if (!provider.isEditing) {
switch (provider.type) {
case RecordType.weight:
provider.updateWeight(0);
break;
case RecordType.sport:
provider.updateSportProject('跑步');
provider.updateSportDuration(0);
break;
case RecordType.sleep:
provider.updateSleepStartTime('');
provider.updateSleepEndTime('');
break;
}
}
}
void _deleteRecord(RecordType type) async{
void _deleteRecord(RecordType type) async {
final provider = context.read<HealthProvider>();
switch(type) {
switch (type) {
case RecordType.weight:
provider.updateWeight(0);
break;
@@ -351,7 +331,9 @@ class _RecordDailyState extends State<RecordDaily> {
String startTime = record.sleepStartTime;
String endTime = record.sleepEndTime;
num difference = calSleepMinDuration(startTime, endTime) - lastValue;
String formattedDiff = NumberFormat('+0.00;-0.00').format(difference / 60);
String formattedDiff = NumberFormat(
'+0.00;-0.00',
).format(difference / 60);
change = '较上次 $formattedDiff ${type.unit}';
break;
}

View File

@@ -3,9 +3,9 @@ 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/form.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class RecordForm extends StatefulWidget {
final RecordType type;
@@ -63,6 +63,88 @@ class _RecordFormState extends State<RecordForm> {
);
}
Widget _buildFormSleepContent(HealthProvider provider) {
final startTime = provider.formItem.sleepStartTime;
final endTime = provider.formItem.sleepEndTime;
return TDCellGroup(
theme: TDCellGroupTheme.cardTheme,
cells: [
TDCell(
title: '入睡时间',
note: startTime,
arrow: true,
leftIcon: Icons.access_alarm,
onClick: (click) {
TDPicker.showDatePicker(
context,
title: '选择时间',
onConfirm: (selected) {
provider.updateSleepStartTime(getTimeFromSelectDate(selected));
final startTime = provider.formItem.sleepStartTime;
final endTime = provider.formItem.sleepEndTime;
calSleepDuration(startTime, endTime);
Navigator.of(context).pop();
},
useYear: false,
useMonth: false,
useDay: false,
useHour: true,
useMinute: true,
useSecond: false,
dateStart: [1999, 01, 01],
dateEnd: [2099, 12, 31],
initialDate: [
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
DateTime.now().hour,
DateTime.now().minute,
],
);
},
),
TDCell(
title: '起床时间',
note: endTime,
arrow: true,
leftIcon: Icons.access_alarm,
onClick: (click) {
TDPicker.showDatePicker(
context,
title: '选择时间',
onConfirm: (selected) {
provider.updateSleepEndTime(getTimeFromSelectDate(selected));
final startTime = provider.formItem.sleepStartTime;
final endTime = provider.formItem.sleepEndTime;
calSleepDuration(startTime, endTime);
Navigator.of(context).pop();
},
useYear: false,
useMonth: false,
useDay: false,
useHour: true,
useMinute: true,
useSecond: false,
dateStart: [1999, 01, 01],
dateEnd: [2099, 12, 31],
initialDate: [
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
DateTime.now().hour,
DateTime.now().minute,
],
);
},
),
],
);
}
Widget _buildFormContent() {
final provider = context.watch<HealthProvider>();
@@ -73,16 +155,24 @@ class _RecordFormState extends State<RecordForm> {
children: [
_buildFormTitle('记录体重'),
SizedBox(height: 20),
buildFormSlider(
context: context,
name: 'slider',
title: '体重Kg',
label: provider.formItem.weight.toString(),
min: 70,
max: 75,
initialValue: 72,
divisions: 50,
onChanged: (value) => provider.updateWeight(value!),
TDSlider(
boxDecoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
sliderThemeData: TDSliderThemeData.capsule(
context: context,
scaleFormatter: (double value) {
return '${value.toStringAsFixed(1)} Kg';
},
showThumbValue: true,
min: AppConfig.minWeight,
max: AppConfig.maxWeight,
),
value: provider.formItem.weight.toDouble(),
leftLabel: AppConfig.minWeight.toString(),
rightLabel: AppConfig.maxWeight.toString(),
onChanged: (value) => provider.updateWeight(value),
),
SizedBox(height: 10),
],
@@ -93,63 +183,62 @@ class _RecordFormState extends State<RecordForm> {
children: [
_buildFormTitle('记录运动'),
SizedBox(height: 24),
buildFormRadioGroup(
context: context,
title: '运动类型',
name: 'sport',
initialValue: provider.formItem.sportProject,
items: AppConfig.sportProjectList,
onChanged: (value) => provider.updateSportProject(value!),
TDRadioGroup(
selectId:
AppConfig.sportProjectList
.indexOf(provider.formItem.sportProject)
.toString(),
passThrough: true,
child: ListView.builder(
padding: const EdgeInsets.all(0),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return TDRadio(
id: index.toString(),
title: AppConfig.sportProjectList[index],
);
},
itemCount: AppConfig.sportProjectList.length,
),
onRadioGroupChange:
(value) => {
provider.updateSportProject(
AppConfig.sportProjectList[int.parse(value!)],
),
},
),
SizedBox(height: 10),
buildFormSlider(
context: context,
name: 'slider',
title: '时长(分钟)',
label: provider.formItem.sportDuration.toString(),
min: 0,
max: 120,
initialValue: 30,
divisions: 12,
onChanged: (value) => provider.updateSportDuration(value!),
TDSlider(
boxDecoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
sliderThemeData: TDSliderThemeData.capsule(
context: context,
scaleFormatter: (double value) {
return '${value.toStringAsFixed(0)} 分钟';
},
showThumbValue: true,
divisions: 11,
min: AppConfig.minSpot,
max: AppConfig.maxSpot,
),
value: provider.formItem.sportDuration.toDouble(),
leftLabel: AppConfig.minSpot.toString(),
rightLabel: AppConfig.maxSpot.toString(),
onChanged: (value) => provider.updateSportDuration(value),
),
SizedBox(height: 10),
],
);
case RecordType.sleep:
final startTime = provider.formItem.sleepStartTime;
final endTime = provider.formItem.sleepEndTime;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFormTitle('记录睡眠'),
SizedBox(height: 24),
buildFormTime(
context: context,
name: 'startTime',
title: '入睡时间',
initialValue: timeOfDayToDateTime(parseTime(startTime)),
onChanged: (time) {
provider.updateSleepStartTime(formatTime(time!));
final startTime = provider.formItem.sleepStartTime;
final endTime = provider.formItem.sleepEndTime;
calSleepDuration(startTime, endTime);
},
),
SizedBox(height: 10),
buildFormTime(
context: context,
name: 'endTime',
title: '起床时间',
initialValue: timeOfDayToDateTime(parseTime(endTime)),
onChanged: (time) {
provider.updateSleepEndTime(formatTime(time!));
final startTime = provider.formItem.sleepStartTime;
final endTime = provider.formItem.sleepEndTime;
calSleepDuration(startTime, endTime);
},
),
_buildFormSleepContent(provider),
SizedBox(height: 10),
_buildSleepDuration(provider.formItem),
SizedBox(height: 10),

View File

@@ -1,9 +1,6 @@
import 'package:fitnote/provider/health_provider.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 Welcome extends StatefulWidget {
const Welcome({super.key});