home_page.dart 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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. ],
  74. ),
  75. const SizedBox(height: 12),
  76. _buildSection(
  77. title: '我的数据',
  78. r: r,
  79. items: [
  80. _EntryItem(
  81. icon: Icons.edit_note,
  82. label: '外出日志',
  83. badge: '${summary.logCount}条',
  84. color: AppColors.success,
  85. onTap: () => context.push('/outing-log/list'),
  86. ),
  87. _EntryItem(
  88. icon: Icons.campaign,
  89. label: '公告通知',
  90. badge: summary.announcementUnread > 0
  91. ? '${summary.announcementUnread}未读'
  92. : '${summary.announcementCount}条',
  93. color: const Color(0xFF722ED1),
  94. onTap: () => context.push('/announcement/list'),
  95. ),
  96. _EntryItem(
  97. icon: Icons.description,
  98. label: '全部单据',
  99. badge: '${summary.totalCount}条',
  100. color: const Color(0xFFEB2F96),
  101. onTap: () {},
  102. ),
  103. ],
  104. ),
  105. ],
  106. ),
  107. );
  108. }
  109. Widget _buildSection({
  110. required String title,
  111. required ResponsiveHelper r,
  112. required List<_EntryItem> items,
  113. }) {
  114. return Container(
  115. padding: const EdgeInsets.all(16),
  116. decoration: BoxDecoration(
  117. color: AppColors.cardWhite,
  118. borderRadius: BorderRadius.circular(12),
  119. boxShadow: [
  120. BoxShadow(
  121. color: Colors.black.withValues(alpha: 0.04),
  122. blurRadius: 4,
  123. offset: const Offset(0, 1),
  124. ),
  125. ],
  126. ),
  127. child: Column(
  128. crossAxisAlignment: CrossAxisAlignment.start,
  129. children: [
  130. Text(
  131. title,
  132. style: const TextStyle(
  133. fontSize: 15,
  134. fontWeight: FontWeight.w600,
  135. color: AppColors.textPrimary,
  136. ),
  137. ),
  138. const SizedBox(height: 14),
  139. if (r.isLandscape)
  140. Row(
  141. mainAxisAlignment: MainAxisAlignment.spaceAround,
  142. children: items
  143. .map((item) => Expanded(child: _buildEntry(item)))
  144. .toList(),
  145. )
  146. else
  147. Row(
  148. mainAxisAlignment: MainAxisAlignment.spaceAround,
  149. children: items.map((item) => _buildEntry(item)).toList(),
  150. ),
  151. ],
  152. ),
  153. );
  154. }
  155. Widget _buildEntry(_EntryItem item) {
  156. return GestureDetector(
  157. onTap: item.onTap,
  158. child: Column(
  159. children: [
  160. Container(
  161. width: 48,
  162. height: 48,
  163. decoration: BoxDecoration(
  164. color: item.color,
  165. borderRadius: BorderRadius.circular(24),
  166. ),
  167. child: Icon(item.icon, color: Colors.white, size: 24),
  168. ),
  169. const SizedBox(height: 8),
  170. Text(
  171. item.label,
  172. style: const TextStyle(
  173. fontSize: 12,
  174. color: AppColors.textSecondary,
  175. ),
  176. ),
  177. const SizedBox(height: 2),
  178. Text(
  179. item.badge,
  180. style: TextStyle(
  181. fontSize: 10,
  182. color: item.badge.contains('0')
  183. ? AppColors.textHint
  184. : AppColors.error,
  185. ),
  186. ),
  187. ],
  188. ),
  189. );
  190. }
  191. }
  192. class _EntryItem {
  193. final IconData icon;
  194. final String label;
  195. final String badge;
  196. final Color color;
  197. final VoidCallback onTap;
  198. const _EntryItem({
  199. required this.icon,
  200. required this.label,
  201. required this.badge,
  202. required this.color,
  203. required this.onTap,
  204. });
  205. }