home_page.dart 7.8 KB

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