home_page.dart 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import 'package:tdesign_flutter/tdesign_flutter.dart';
  6. import '../../core/theme/app_colors.dart';
  7. import '../../core/utils/responsive.dart';
  8. import 'home_controller.dart';
  9. class HomePage extends ConsumerWidget {
  10. const HomePage({super.key});
  11. @override
  12. Widget build(BuildContext context, WidgetRef ref) {
  13. final summaryAsync = ref.watch(homeSummaryProvider);
  14. return Scaffold(
  15. appBar: TDNavBar(
  16. title: 'TBOSS · 工作台',
  17. titleColor: Colors.white,
  18. backgroundColor: const Color(0xFF00ABF3),
  19. centerTitle: false,
  20. useDefaultBack: false,
  21. leftBarItems: [TDNavBarItem(icon: Icons.close, iconColor: Colors.white, action: () => SystemNavigator.pop())],
  22. ),
  23. body: summaryAsync.when(
  24. loading: () =>
  25. const Center(child: CircularProgressIndicator()),
  26. error: (_, __) => const Center(child: Text('加载失败')),
  27. data: (summary) => LayoutBuilder(
  28. builder: (context, constraints) {
  29. final r = ResponsiveHelper(
  30. width: constraints.maxWidth,
  31. height: constraints.maxHeight,
  32. isLandscape: constraints.maxWidth > constraints.maxHeight,
  33. isWide: constraints.maxWidth > 600,
  34. );
  35. return _buildContent(context, summary, r);
  36. },
  37. ),
  38. ),
  39. );
  40. }
  41. Widget _buildContent(
  42. BuildContext context, HomeSummary summary, ResponsiveHelper r) {
  43. return SingleChildScrollView(
  44. padding: const EdgeInsets.all(12),
  45. child: Column(
  46. children: [
  47. _buildSection(
  48. title: '我的审批',
  49. r: r,
  50. items: [
  51. _EntryItem(
  52. icon: Icons.receipt_long,
  53. label: '报销单',
  54. badge: '${summary.expensePending}待办',
  55. color: AppColors.primary,
  56. onTap: () => context.push('/expense/list'),
  57. ),
  58. _EntryItem(
  59. icon: Icons.access_time,
  60. label: '加班',
  61. badge: '${summary.overtimePending}待办',
  62. color: AppColors.primary,
  63. onTap: () => context.push('/overtime/list'),
  64. ),
  65. _EntryItem(
  66. icon: Icons.directions_car,
  67. label: '用车',
  68. badge: '${summary.vehiclePending}待办',
  69. color: AppColors.warning,
  70. onTap: () => context.push('/vehicle/list'),
  71. ),
  72. _EntryItem(
  73. icon: Icons.assignment,
  74. label: '报销申请',
  75. badge: summary.expenseApplyPending > 0
  76. ? '${summary.expenseApplyPending}待办'
  77. : '0待办',
  78. color: Colors.deepPurple,
  79. onTap: () => context.push('/expense-apply/list'),
  80. ),
  81. ],
  82. ),
  83. const SizedBox(height: 12),
  84. _buildSection(
  85. title: '我的数据',
  86. r: r,
  87. items: [
  88. _EntryItem(
  89. icon: Icons.edit_note,
  90. label: '外出日志',
  91. badge: '${summary.logCount}条',
  92. color: AppColors.success,
  93. onTap: () => context.push('/outing-log/list'),
  94. ),
  95. _EntryItem(
  96. icon: Icons.campaign,
  97. label: '公告通知',
  98. badge: summary.announcementUnread > 0
  99. ? '${summary.announcementUnread}未读'
  100. : '${summary.announcementCount}条',
  101. color: const Color(0xFF722ED1),
  102. onTap: () => context.push('/announcement/list'),
  103. ),
  104. ],
  105. ),
  106. const SizedBox(height: 12),
  107. _buildSection(
  108. title: '报表中心',
  109. r: r,
  110. items: [
  111. _EntryItem(
  112. icon: Icons.pie_chart,
  113. label: '报销明细表',
  114. badge: '',
  115. color: AppColors.primary,
  116. onTap: () => context.push('/report/expense-detail'),
  117. ),
  118. _EntryItem(
  119. icon: Icons.bar_chart,
  120. label: '加班明细表',
  121. badge: '',
  122. color: AppColors.success,
  123. onTap: () => context.push('/report/overtime-detail'),
  124. ),
  125. _EntryItem(
  126. icon: Icons.directions_car,
  127. label: '用车明细表',
  128. badge: '',
  129. color: AppColors.warning,
  130. onTap: () => context.push('/report/vehicle-detail'),
  131. ),
  132. _EntryItem(
  133. icon: Icons.receipt,
  134. label: '报销申请明细表',
  135. badge: '',
  136. color: Colors.deepPurple,
  137. onTap: () => context.push('/report/expense-apply-detail'),
  138. ),
  139. ],
  140. ),
  141. ],
  142. ),
  143. );
  144. }
  145. Widget _buildSection({
  146. required String title,
  147. required ResponsiveHelper r,
  148. required List<_EntryItem> items,
  149. }) {
  150. return Container(
  151. padding: const EdgeInsets.all(16),
  152. decoration: BoxDecoration(
  153. color: AppColors.cardWhite,
  154. borderRadius: BorderRadius.circular(12),
  155. boxShadow: [
  156. BoxShadow(
  157. color: Colors.black.withValues(alpha: 0.04),
  158. blurRadius: 4,
  159. offset: const Offset(0, 1),
  160. ),
  161. ],
  162. ),
  163. child: Column(
  164. crossAxisAlignment: CrossAxisAlignment.start,
  165. children: [
  166. Text(
  167. title,
  168. style: const TextStyle(
  169. fontSize: 15,
  170. fontWeight: FontWeight.w600,
  171. color: AppColors.textPrimary,
  172. ),
  173. ),
  174. const SizedBox(height: 14),
  175. if (r.isLandscape)
  176. Row(
  177. mainAxisAlignment: MainAxisAlignment.spaceAround,
  178. children: items
  179. .map((item) => Expanded(child: _buildEntry(item)))
  180. .toList(),
  181. )
  182. else
  183. Row(
  184. mainAxisAlignment: MainAxisAlignment.spaceAround,
  185. children: items.map((item) => _buildEntry(item)).toList(),
  186. ),
  187. ],
  188. ),
  189. );
  190. }
  191. Widget _buildEntry(_EntryItem item) {
  192. return GestureDetector(
  193. onTap: item.onTap,
  194. child: Column(
  195. children: [
  196. Container(
  197. width: 48,
  198. height: 48,
  199. decoration: BoxDecoration(
  200. color: item.color,
  201. borderRadius: BorderRadius.circular(24),
  202. ),
  203. child: Icon(item.icon, color: Colors.white, size: 24),
  204. ),
  205. const SizedBox(height: 8),
  206. Text(
  207. item.label,
  208. style: const TextStyle(
  209. fontSize: 12,
  210. color: AppColors.textSecondary,
  211. ),
  212. ),
  213. const SizedBox(height: 2),
  214. Text(
  215. item.badge,
  216. style: TextStyle(
  217. fontSize: 10,
  218. color: item.badge.contains('0')
  219. ? AppColors.textHint
  220. : AppColors.error,
  221. ),
  222. ),
  223. ],
  224. ),
  225. );
  226. }
  227. }
  228. class _EntryItem {
  229. final IconData icon;
  230. final String label;
  231. final String badge;
  232. final Color color;
  233. final VoidCallback onTap;
  234. const _EntryItem({
  235. required this.icon,
  236. required this.label,
  237. required this.badge,
  238. required this.color,
  239. required this.onTap,
  240. });
  241. }