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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:provider/provider.dart';
  4. import '../../../shared/services/cache_service.dart';
  5. import '../services/auth_provider.dart';
  6. /// 登录页面
  7. ///
  8. /// 支持表单校验、密码可见切换、记住用户名(通过 CacheService 持久化)、加载态。
  9. class LoginPage extends StatefulWidget {
  10. const LoginPage({super.key});
  11. @override
  12. State<LoginPage> createState() => _LoginPageState();
  13. }
  14. class _LoginPageState extends State<LoginPage> {
  15. static const String _kRememberKey = 'login:remember_username';
  16. final _formKey = GlobalKey<FormState>();
  17. final _usernameController = TextEditingController();
  18. final _passwordController = TextEditingController();
  19. final _cache = CacheService();
  20. bool _obscurePassword = true;
  21. bool _rememberUsername = true;
  22. bool _restored = false;
  23. @override
  24. void initState() {
  25. super.initState();
  26. _restoreRememberedUsername();
  27. }
  28. /// 从本地缓存恢复上次记住的用户名
  29. Future<void> _restoreRememberedUsername() async {
  30. final saved = await _cache.get<String>(_kRememberKey, box: 'auth');
  31. if (saved != null && saved.isNotEmpty && mounted) {
  32. setState(() {
  33. _usernameController.text = saved;
  34. _restored = true;
  35. });
  36. }
  37. }
  38. /// 持久化/清除记住的用户名
  39. Future<void> _persistRememberedUsername(String username) async {
  40. if (_rememberUsername) {
  41. await _cache.put(_kRememberKey, username, box: 'auth');
  42. } else {
  43. await _cache.delete(_kRememberKey, box: 'auth');
  44. }
  45. }
  46. @override
  47. void dispose() {
  48. _usernameController.dispose();
  49. _passwordController.dispose();
  50. super.dispose();
  51. }
  52. Future<void> _handleLogin() async {
  53. if (!_formKey.currentState!.validate()) return;
  54. final username = _usernameController.text.trim();
  55. final authProvider = Provider.of<AuthProvider>(context, listen: false);
  56. final success = await authProvider.login(
  57. username: username,
  58. password: _passwordController.text,
  59. );
  60. if (success && mounted) {
  61. // 登录成功:按勾选状态持久化用户名
  62. await _persistRememberedUsername(username);
  63. // 登录成功后路由由 AuthGuard 自动处理
  64. GoRouter.of(context).go('/main');
  65. } else if (mounted) {
  66. ScaffoldMessenger.of(context).showSnackBar(
  67. SnackBar(
  68. content: Text(authProvider.errorMessage ?? '登录失败'),
  69. backgroundColor: Colors.red,
  70. ),
  71. );
  72. }
  73. }
  74. @override
  75. Widget build(BuildContext context) {
  76. return Scaffold(
  77. body: SafeArea(
  78. child: Center(
  79. child: SingleChildScrollView(
  80. padding: const EdgeInsets.all(32),
  81. child: Form(
  82. key: _formKey,
  83. child: Column(
  84. mainAxisAlignment: MainAxisAlignment.center,
  85. crossAxisAlignment: CrossAxisAlignment.stretch,
  86. children: [
  87. // Logo 和应用名
  88. const Icon(
  89. Icons.water_drop,
  90. size: 80,
  91. color: Color(0xFF1976D2),
  92. ),
  93. const SizedBox(height: 16),
  94. const Text(
  95. '供水管理系统',
  96. textAlign: TextAlign.center,
  97. style: TextStyle(
  98. fontSize: 24,
  99. fontWeight: FontWeight.bold,
  100. color: Color(0xFF1976D2),
  101. ),
  102. ),
  103. const SizedBox(height: 8),
  104. Text(
  105. '供水 · 巡检 · 营收',
  106. textAlign: TextAlign.center,
  107. style: TextStyle(
  108. fontSize: 14,
  109. color: Colors.grey[600],
  110. ),
  111. ),
  112. const SizedBox(height: 48),
  113. // 用户名输入
  114. TextFormField(
  115. controller: _usernameController,
  116. decoration: const InputDecoration(
  117. labelText: '用户名',
  118. prefixIcon: Icon(Icons.person),
  119. hintText: '请输入用户名',
  120. ),
  121. validator: (value) {
  122. if (value == null || value.trim().isEmpty) {
  123. return '请输入用户名';
  124. }
  125. return null;
  126. },
  127. ),
  128. const SizedBox(height: 16),
  129. // 密码输入
  130. TextFormField(
  131. controller: _passwordController,
  132. decoration: InputDecoration(
  133. labelText: '密码',
  134. prefixIcon: const Icon(Icons.lock),
  135. hintText: '请输入密码',
  136. suffixIcon: IconButton(
  137. icon: Icon(
  138. _obscurePassword ? Icons.visibility_off : Icons.visibility,
  139. ),
  140. onPressed: () {
  141. setState(() {
  142. _obscurePassword = !_obscurePassword;
  143. });
  144. },
  145. ),
  146. ),
  147. obscureText: _obscurePassword,
  148. validator: (value) {
  149. if (value == null || value.isEmpty) {
  150. return '请输入密码';
  151. }
  152. if (value.length < 6) {
  153. return '密码长度不能少于6位';
  154. }
  155. return null;
  156. },
  157. ),
  158. const SizedBox(height: 12),
  159. // 记住用户名
  160. Row(
  161. children: [
  162. Checkbox(
  163. value: _rememberUsername,
  164. onChanged: (v) => setState(() => _rememberUsername = v ?? false),
  165. ),
  166. const Text('记住用户名'),
  167. ],
  168. ),
  169. const SizedBox(height: 20),
  170. // 登录按钮
  171. Consumer<AuthProvider>(
  172. builder: (context, auth, child) {
  173. return ElevatedButton(
  174. onPressed: auth.isLoading ? null : _handleLogin,
  175. child: auth.isLoading
  176. ? const SizedBox(
  177. height: 20,
  178. width: 20,
  179. child: CircularProgressIndicator(
  180. strokeWidth: 2,
  181. color: Colors.white,
  182. ),
  183. )
  184. : const Text(
  185. '登 录',
  186. style: TextStyle(fontSize: 16),
  187. ),
  188. );
  189. },
  190. ),
  191. const SizedBox(height: 16),
  192. // 版本信息
  193. Text(
  194. 'v1.0.0',
  195. textAlign: TextAlign.center,
  196. style: TextStyle(color: Colors.grey[400], fontSize: 12),
  197. ),
  198. ],
  199. ),
  200. ),
  201. ),
  202. ),
  203. ),
  204. );
  205. }
  206. }