diff --git a/lib/views/stats.dart b/lib/views/stats.dart index 6eef7a7..6e104bc 100644 --- a/lib/views/stats.dart +++ b/lib/views/stats.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:food_hub_app/widgets/stats/card.dart'; import 'package:graphic/graphic.dart'; import 'data.dart'; @@ -14,88 +15,210 @@ class _StatsPage extends State { var _smooth = false; var _stepped = false; + final List> chartData = const [ + {'x': '1月', 'y': 30}, + {'x': '2月', 'y': 45}, + {'x': '3月', 'y': 28}, + {'x': '4月', 'y': 55}, + {'x': '5月', 'y': 40}, + {'x': '6月', 'y': 65}, + ]; + + // @override + // Widget build(BuildContext context) { + // return SingleChildScrollView( + // child: Center( + // child: Column( + // children: [ + // Container( + // margin: const EdgeInsets.only(top: 10), + // width: 350, + // height: 300, + // child: Chart( + // data: basicData, + // variables: { + // 'genre': Variable( + // accessor: (Map map) => map['genre'] as String, + // ), + // 'sold': Variable(accessor: (Map map) => map['sold'] as num), + // }, + // marks: [ + // IntervalMark( + // label: LabelEncode( + // encoder: (tuple) => Label(tuple['sold'].toString()), + // ), + // elevation: ElevationEncode( + // value: 0, + // updaters: { + // 'tap': {true: (_) => 5}, + // }, + // ), + // color: ColorEncode( + // value: Defaults.primaryColor, + // updaters: { + // 'tap': {false: (color) => color.withAlpha(100)}, + // }, + // ), + // ), + // ], + // axes: [Defaults.horizontalAxis, Defaults.verticalAxis], + // selections: {'tap': PointSelection(dim: Dim.x)}, + // tooltip: TooltipGuide(), + // crosshair: CrosshairGuide(), + // ), + // ), + // Container( + // padding: const EdgeInsets.fromLTRB(20, 40, 20, 5), + // child: const Text( + // 'Transposed Bar Chart', + // style: TextStyle(fontSize: 20), + // ), + // ), + // Container( + // margin: const EdgeInsets.only(top: 10), + // width: 350, + // height: 300, + // child: Chart( + // data: basicData, + // variables: { + // 'genre': Variable( + // accessor: (Map map) => map['genre'] as String, + // ), + // 'sold': Variable(accessor: (Map map) => map['sold'] as num), + // }, + // transforms: [Proportion(variable: 'sold', as: 'percent')], + // marks: [ + // IntervalMark( + // position: Varset('percent') / Varset('genre'), + // label: LabelEncode( + // encoder: (tuple) => Label(tuple['sold'].toString()), + // ), + // color: ColorEncode( + // variable: 'genre', + // values: Defaults.colors10, + // ), + // modifiers: [StackModifier()], + // ), + // ], + // coord: PolarCoord(transposed: true, dimCount: 1, dimFill: 1.05), + // ), + // ), + // ], + // ), + // ), + // ); + // } + @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Center( - child: Column( - children: [ - Container( - margin: const EdgeInsets.only(top: 10), - width: 350, - height: 300, - child: Chart( - data: basicData, - variables: { - 'genre': Variable( - accessor: (Map map) => map['genre'] as String, - ), - 'sold': Variable(accessor: (Map map) => map['sold'] as num), - }, - marks: [ - IntervalMark( - label: LabelEncode( - encoder: (tuple) => Label(tuple['sold'].toString()), + return Padding( + padding: EdgeInsets.all(10), + child: Column( + children: [ + GridView.count( + shrinkWrap: true, + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 3, + children: const [ + StatisticCard( + icon: Icons.restaurant_menu, + color: Colors.blue, + title: '菜谱总数', + value: 19, + unit: '个', + ), + StatisticCard( + icon: Icons.grid_view_rounded, + color: Colors.green, + title: '菜谱类别', + value: 5, + unit: '种', + ), + StatisticCard( + icon: Icons.flag, + color: Colors.orange, + title: '做菜次数', + value: 25, + unit: '个', + ), + StatisticCard( + icon: Icons.show_chart, + color: Colors.red, + title: '平均次数', + value: 3.13, + unit: '次/月', + ), + ], + ), + const SizedBox(height: 10), + Expanded( + child: Card( + color: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: EdgeInsets.all(10), + child: Column( + children: [ + _buildTitleSection(), + Divider( + height: 1, + thickness: 1, + color: Theme.of(context).primaryColor, + indent: 0, + endIndent: 0, ), - elevation: ElevationEncode( - value: 0, - updaters: { - 'tap': {true: (_) => 5}, - }, - ), - color: ColorEncode( - value: Defaults.primaryColor, - updaters: { - 'tap': {false: (color) => color.withAlpha(100)}, - }, - ), - ), - ], - axes: [Defaults.horizontalAxis, Defaults.verticalAxis], - selections: {'tap': PointSelection(dim: Dim.x)}, - tooltip: TooltipGuide(), - crosshair: CrosshairGuide(), + // 下半部分:图表区 + Expanded(child: _buildLineChart()), + ], + ), ), ), - Container( - padding: const EdgeInsets.fromLTRB(20, 40, 20, 5), - child: const Text( - 'Transposed Bar Chart', - style: TextStyle(fontSize: 20), - ), - ), - Container( - margin: const EdgeInsets.only(top: 10), - width: 350, - height: 300, - child: Chart( - data: basicData, - variables: { - 'genre': Variable( - accessor: (Map map) => map['genre'] as String, - ), - 'sold': Variable(accessor: (Map map) => map['sold'] as num), - }, - transforms: [Proportion(variable: 'sold', as: 'percent')], - marks: [ - IntervalMark( - position: Varset('percent') / Varset('genre'), - label: LabelEncode( - encoder: (tuple) => Label(tuple['sold'].toString()), - ), - color: ColorEncode( - variable: 'genre', - values: Defaults.colors10, - ), - modifiers: [StackModifier()], - ), - ], - coord: PolarCoord(transposed: true, dimCount: 1, dimFill: 1.05), - ), - ), - ], - ), + ), + ], ), ); } + + // 构建标题区域 + Widget _buildTitleSection() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: const [ + Icon(Icons.show_chart, size: 24), + SizedBox(width: 5), + Text('月度数据趋势统计', style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ); + } + + Widget _buildLineChart() { + return Chart( + data: chartData, + variables: { + 'date': Variable( + accessor: (Map map) => map['x'] as String, + scale: OrdinalScale( + tickCount: 5, // x轴刻度数量 + ), + ), + 'points': Variable( + accessor: (Map map) => (map['y'] ?? double.nan) as num, + ), + }, + marks: [ + LineMark( + shape: ShapeEncode(value: BasicLineShape(smooth: false)), + size: SizeEncode(value: 1.5), + ), + ], + axes: [Defaults.horizontalAxis, Defaults.verticalAxis] + ); + } } diff --git a/lib/widgets/common/index.dart b/lib/widgets/common/index.dart index 39d87a6..41d79a7 100644 --- a/lib/widgets/common/index.dart +++ b/lib/widgets/common/index.dart @@ -27,6 +27,25 @@ InputDecoration formInputDecoration({ ); } +Widget circleIconButton({ + required IconData icon, + required VoidCallback onPressed, + required BuildContext context, +}) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + fixedSize: Size(32, 32), + shape: CircleBorder(), + elevation: 0, + backgroundColor: Theme.of(context).primaryColor, + padding: EdgeInsets.zero, + minimumSize: const Size(0, 0), + ), + child: Icon(icon, color: Colors.white, size: 18), + ); +} + /// 确认按钮样式 ButtonStyle primaryButtonStyle() { return ButtonStyle( diff --git a/lib/widgets/common/year_selector.dart b/lib/widgets/common/year_selector.dart index 41bcd23..1c8b751 100644 --- a/lib/widgets/common/year_selector.dart +++ b/lib/widgets/common/year_selector.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:food_hub_app/widgets/common/index.dart'; class YearSelector extends StatefulWidget { final int initialYear; @@ -88,15 +88,11 @@ class _YearSelectorState extends State return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - TDButton( + circleIconButton( + context: context, icon: Icons.chevron_left, - size: TDButtonSize.small, - type: TDButtonType.fill, - shape: TDButtonShape.circle, - theme: TDButtonTheme.primary, - onTap: () => _previousYear(), + onPressed: () => _previousYear(), ), - SizedBox(width: 10), // 年份显示 AnimatedBuilder( animation: _scaleAnimation, @@ -112,16 +108,12 @@ class _YearSelectorState extends State ), ), ), - SizedBox(width: 10), - TDButton( + circleIconButton( + context: context, icon: Icons.chevron_right, - size: TDButtonSize.small, - type: TDButtonType.fill, - shape: TDButtonShape.circle, - theme: TDButtonTheme.primary, - onTap: () => _nextYear(), - ), + onPressed: () => _nextYear(), + ) ], ); } -} \ No newline at end of file +} diff --git a/lib/widgets/recipe/calendar.dart b/lib/widgets/recipe/calendar.dart index cb5a470..0e0da84 100644 --- a/lib/widgets/recipe/calendar.dart +++ b/lib/widgets/recipe/calendar.dart @@ -3,6 +3,7 @@ import 'package:food_hub_app/api/recipe.dart'; import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/utils/date_util.dart'; import 'package:food_hub_app/utils/index.dart'; +import 'package:food_hub_app/widgets/common/index.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; @@ -145,12 +146,10 @@ class _RecipeCalendarState extends State { ], ), ), - SizedBox(width: 10), - TDButton( - icon: TDIcons.arrow_right, - type: TDButtonType.fill, - shape: TDButtonShape.circle, - theme: TDButtonTheme.primary, + circleIconButton( + context: context, + icon: Icons.chevron_right, + onPressed: () => {} ), ], ), diff --git a/lib/widgets/recipe/card.dart b/lib/widgets/recipe/card.dart index b8288c2..88fb43e 100644 --- a/lib/widgets/recipe/card.dart +++ b/lib/widgets/recipe/card.dart @@ -4,7 +4,7 @@ import 'package:food_hub_app/config/app_config.dart'; import 'package:food_hub_app/models/recipe.dart'; import 'package:food_hub_app/widgets/common/image.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:food_hub_app/widgets/common/index.dart'; class RecipeCard extends StatelessWidget { final Recipe recipe; @@ -92,22 +92,15 @@ class RecipeCard extends StatelessWidget { ), Row( children: [ - TDButton( + circleIconButton( + context: context, icon: Icons.edit, - size: TDButtonSize.small, - type: TDButtonType.fill, - shape: TDButtonShape.circle, - theme: TDButtonTheme.primary, - onTap: () => {}, + onPressed: () => {}, ), - SizedBox(width: 10), - TDButton( + circleIconButton( + context: context, icon: Icons.book, - size: TDButtonSize.small, - type: TDButtonType.fill, - shape: TDButtonShape.circle, - theme: TDButtonTheme.primary, - onTap: () => {}, + onPressed: () => {}, ), ], ), diff --git a/lib/widgets/recipe/timeline.dart b/lib/widgets/recipe/timeline.dart index 57a8a31..b664584 100644 --- a/lib/widgets/recipe/timeline.dart +++ b/lib/widgets/recipe/timeline.dart @@ -60,9 +60,15 @@ Widget timelineContainer(List recordList) { builder: TimelineTileBuilder.connected( itemCount: recordList.length, connectorBuilder: - (context, index, type) => Connector.solidLine(thickness: 2), + (context, index, type) => Connector.solidLine( + thickness: 2, + color: Theme.of(context).primaryColor, + ), indicatorBuilder: (context, index) { - return Indicator.dot(size: 12.0); + return Indicator.dot( + size: 12.0, + color: Theme.of(context).primaryColor, + ); }, contentsBuilder: (context, index) { return TimelineCard(record: recordList[index]); diff --git a/lib/widgets/stats/card.dart b/lib/widgets/stats/card.dart new file mode 100644 index 0000000..914ac2c --- /dev/null +++ b/lib/widgets/stats/card.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class StatisticCard extends StatelessWidget { + final IconData icon; + final Color color; + final String title; + final num value; + final String unit; + + const StatisticCard({ + super.key, + required this.icon, + required this.color, + required this.title, + required this.value, + required this.unit, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + SizedBox( + width: 60, + height: 60, + child: Icon(icon, color: color, size: 42), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title, style: TextStyle(fontSize: 20, color: color)), + const SizedBox(height: 4), + Row( + children: [ + Text( + value.toString(), + style: TextStyle(fontSize: 20, color: color), + ), + const SizedBox(width: 5), + Text(unit, style: TextStyle(fontSize: 20, color: color)), + ], + ), + ], + ), + ], + ), + ), + ); + } +}