智慧水务管理系统 - 精河县供水工程综合管理平台

profile_page.dart 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:provider/provider.dart';
  4. import '../../auth/models/user_model.dart';
  5. import '../../auth/services/auth_provider.dart';
  6. import '../../../config/app_routes.dart';
  7. /// 个人中心页面(对应 Issue #79:个人中心)
  8. ///
  9. /// 展示当前登录用户信息 + 功能菜单(个人信息/消息/设置/帮助/关于)+ 退出登录。
  10. class ProfilePage extends StatelessWidget {
  11. const ProfilePage({super.key});
  12. @override
  13. Widget build(BuildContext context) {
  14. return Scaffold(
  15. appBar: AppBar(title: const Text('个人中心')),
  16. body: Consumer<AuthProvider>(
  17. builder: (context, auth, _) {
  18. final user = auth.currentUser;
  19. return ListView(
  20. children: [
  21. _UserHeader(user: user),
  22. const SizedBox(height: 8),
  23. _MenuGroup(items: const [
  24. _MenuItem(icon: Icons.person_outline, title: '个人信息'),
  25. _MenuItem(icon: Icons.notifications_outlined, title: '我的消息'),
  26. _MenuItem(icon: Icons.settings_outlined, title: '设置'),
  27. ]),
  28. const SizedBox(height: 8),
  29. _MenuGroup(items: const [
  30. _MenuItem(icon: Icons.help_outline, title: '帮助与反馈'),
  31. _MenuItem(icon: Icons.info_outline, title: '关于'),
  32. ]),
  33. const SizedBox(height: 16),
  34. _LogoutButton(onLogout: () => _handleLogout(context, auth)),
  35. const SizedBox(height: 24),
  36. ],
  37. );
  38. },
  39. ),
  40. );
  41. }
  42. Future<void> _handleLogout(BuildContext context, AuthProvider auth) async {
  43. final confirmed = await showDialog<bool>(
  44. context: context,
  45. builder: (ctx) => AlertDialog(
  46. title: const Text('退出登录'),
  47. content: const Text('确定要退出当前账号吗?'),
  48. actions: [
  49. TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消')),
  50. TextButton(
  51. onPressed: () => Navigator.of(ctx).pop(true),
  52. child: const Text('退出', style: TextStyle(color: Colors.red)),
  53. ),
  54. ],
  55. ),
  56. );
  57. if (confirmed == true) {
  58. await auth.logout();
  59. // 登出后跳转登录页(go_router 守卫也会拦截,这里显式跳转)
  60. if (context.mounted) {
  61. GoRouter.of(context).go(AppRoutes.login);
  62. }
  63. }
  64. }
  65. }
  66. /// 用户信息头部(头像 + 姓名 + 角色/部门)
  67. class _UserHeader extends StatelessWidget {
  68. final UserModel? user;
  69. const _UserHeader({this.user});
  70. @override
  71. Widget build(BuildContext context) {
  72. final name = user?.name ?? '未登录';
  73. final role = user?.role ?? '';
  74. final department = user?.department;
  75. final phone = user?.phone;
  76. return Container(
  77. width: double.infinity,
  78. padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
  79. color: Theme.of(context).colorScheme.primary,
  80. child: Column(
  81. children: [
  82. CircleAvatar(
  83. radius: 40,
  84. backgroundColor: Colors.white24,
  85. child: _buildAvatar(user, name),
  86. ),
  87. const SizedBox(height: 12),
  88. Text(name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white)),
  89. if (role.isNotEmpty) ...[
  90. const SizedBox(height: 4),
  91. Text(_roleLabel(role), style: const TextStyle(color: Colors.white70)),
  92. ],
  93. if (department != null || phone != null) ...[
  94. const SizedBox(height: 4),
  95. Text(
  96. [department, phone].whereType<String>().join(' · '),
  97. style: const TextStyle(color: Colors.white60, fontSize: 13),
  98. ),
  99. ],
  100. ],
  101. ),
  102. );
  103. }
  104. String _roleLabel(String role) {
  105. const map = {'admin': '管理员', 'inspector': '巡检员', 'operator': '操作员'};
  106. return map[role] ?? role;
  107. }
  108. /// 头像:有 avatar 用网络图,否则取姓名首字
  109. Widget _buildAvatar(UserModel? user, String name) {
  110. final avatar = user?.avatar;
  111. if (avatar != null && avatar.isNotEmpty) {
  112. return ClipOval(child: Image.network(avatar, width: 80, height: 80, fit: BoxFit.cover));
  113. }
  114. return Text(
  115. name.isNotEmpty ? name.substring(0, 1) : '?',
  116. style: const TextStyle(fontSize: 32, color: Colors.white),
  117. );
  118. }
  119. }
  120. /// 菜单组(带卡片容器)
  121. class _MenuGroup extends StatelessWidget {
  122. final List<_MenuItem> items;
  123. const _MenuGroup({required this.items});
  124. @override
  125. Widget build(BuildContext context) {
  126. return Card(
  127. margin: const EdgeInsets.symmetric(horizontal: 12),
  128. child: Column(
  129. children: [
  130. for (int i = 0; i < items.length; i++) ...[
  131. items[i],
  132. if (i < items.length - 1) const Divider(height: 1, indent: 56),
  133. ],
  134. ],
  135. ),
  136. );
  137. }
  138. }
  139. /// 单个菜单项(占位 onTap,待后续接入具体页面)
  140. class _MenuItem extends StatelessWidget {
  141. final IconData icon;
  142. final String title;
  143. final VoidCallback? onTap;
  144. const _MenuItem({required this.icon, required this.title, this.onTap});
  145. @override
  146. Widget build(BuildContext context) {
  147. return ListTile(
  148. leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
  149. title: Text(title),
  150. trailing: const Icon(Icons.chevron_right, color: Colors.grey),
  151. onTap: onTap ?? () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$title(待实现)'))),
  152. );
  153. }
  154. }
  155. /// 退出登录按钮
  156. class _LogoutButton extends StatelessWidget {
  157. final VoidCallback onLogout;
  158. const _LogoutButton({required this.onLogout});
  159. @override
  160. Widget build(BuildContext context) {
  161. return Padding(
  162. padding: const EdgeInsets.symmetric(horizontal: 12),
  163. child: OutlinedButton.icon(
  164. onPressed: onLogout,
  165. icon: const Icon(Icons.logout, color: Colors.red),
  166. label: const Text('退出登录', style: TextStyle(color: Colors.red)),
  167. style: OutlinedButton.styleFrom(
  168. minimumSize: const Size(double.infinity, 48),
  169. side: const BorderSide(color: Colors.red),
  170. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
  171. ),
  172. ),
  173. );
  174. }
  175. }