login_page.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:sino_med_cloud/l10n/app_localizations.dart';
  5. class LoginPage extends ConsumerStatefulWidget {
  6. const LoginPage({super.key});
  7. @override
  8. ConsumerState<LoginPage> createState() => _LoginPageState();
  9. }
  10. class _LoginPageState extends ConsumerState<LoginPage>
  11. with SingleTickerProviderStateMixin {
  12. late TabController _tabController;
  13. final _passwordFormKey = GlobalKey<FormState>();
  14. final _smsFormKey = GlobalKey<FormState>();
  15. // 密码登录表单
  16. final _phoneController = TextEditingController();
  17. final _passwordController = TextEditingController();
  18. bool _obscurePassword = true;
  19. // 验证码登录表单
  20. final _phoneSmsController = TextEditingController();
  21. final _smsCodeController = TextEditingController();
  22. int _countdown = 0;
  23. @override
  24. void initState() {
  25. super.initState();
  26. _tabController = TabController(length: 2, vsync: this);
  27. }
  28. @override
  29. void dispose() {
  30. _tabController.dispose();
  31. _phoneController.dispose();
  32. _passwordController.dispose();
  33. _phoneSmsController.dispose();
  34. _smsCodeController.dispose();
  35. super.dispose();
  36. }
  37. // 发送验证码
  38. void _sendSmsCode() {
  39. final l10n = AppLocalizations.of(context)!;
  40. if (_phoneSmsController.text.isEmpty) {
  41. ScaffoldMessenger.of(context).showSnackBar(
  42. SnackBar(content: Text(l10n.phoneNumberRequiredForSms)),
  43. );
  44. return;
  45. }
  46. // TODO: 调用发送验证码接口
  47. setState(() {
  48. _countdown = 60;
  49. });
  50. // 倒计时
  51. Future.doWhile(() async {
  52. await Future.delayed(const Duration(seconds: 1));
  53. if (mounted) {
  54. setState(() {
  55. _countdown--;
  56. });
  57. }
  58. return _countdown > 0;
  59. });
  60. }
  61. // 密码登录
  62. void _handlePasswordLogin() {
  63. final l10n = AppLocalizations.of(context)!;
  64. if (_passwordFormKey.currentState!.validate()) {
  65. // TODO: 调用登录接口
  66. // ScaffoldMessenger.of(context).showSnackBar(
  67. // SnackBar(content: Text(l10n.loginNotImplemented)),
  68. // );
  69. context.go('/mainTab');
  70. }
  71. }
  72. // 验证码登录
  73. void _handleSmsLogin() {
  74. final l10n = AppLocalizations.of(context)!;
  75. if (_smsFormKey.currentState!.validate()) {
  76. // TODO: 调用登录接口
  77. // ScaffoldMessenger.of(context).showSnackBar(
  78. // SnackBar(content: Text(l10n.loginNotImplemented)),
  79. // );
  80. context.go('/mainTab');
  81. }
  82. }
  83. @override
  84. Widget build(BuildContext context) {
  85. final l10n = AppLocalizations.of(context)!;
  86. return Scaffold(
  87. body: GestureDetector(
  88. onTap: () {
  89. // 点击空白区域时收起键盘并移除焦点
  90. FocusScope.of(context).unfocus();
  91. },
  92. behavior: HitTestBehavior.opaque,
  93. child: SafeArea(
  94. child: SingleChildScrollView(
  95. padding: const EdgeInsets.all(24),
  96. child: Column(
  97. crossAxisAlignment: CrossAxisAlignment.stretch,
  98. children: [
  99. const SizedBox(height: 40),
  100. // Logo 或标题
  101. Text(
  102. l10n.appName,
  103. style: const TextStyle(
  104. fontSize: 32,
  105. fontWeight: FontWeight.bold,
  106. color: Color(0xFF1F2937),
  107. ),
  108. textAlign: TextAlign.center,
  109. ),
  110. const SizedBox(height: 8),
  111. Text(
  112. l10n.appSubtitle,
  113. style: const TextStyle(
  114. fontSize: 16,
  115. color: Color(0xFF6B7280),
  116. ),
  117. textAlign: TextAlign.center,
  118. ),
  119. const SizedBox(height: 48),
  120. // Tab 切换
  121. Container(
  122. decoration: BoxDecoration(
  123. color: Colors.white,
  124. borderRadius: BorderRadius.circular(12),
  125. ),
  126. child: TabBar(
  127. controller: _tabController,
  128. indicator: BoxDecoration(
  129. borderRadius: BorderRadius.circular(12),
  130. color: const Color(0xFF00BFA5),
  131. ),
  132. indicatorSize: TabBarIndicatorSize.tab,
  133. dividerColor: Colors.transparent,
  134. labelColor: Colors.white,
  135. unselectedLabelColor: const Color(0xFF6B7280),
  136. labelStyle: const TextStyle(
  137. fontSize: 16,
  138. fontWeight: FontWeight.w600,
  139. ),
  140. unselectedLabelStyle: const TextStyle(
  141. fontSize: 16,
  142. fontWeight: FontWeight.w500,
  143. ),
  144. tabs: [
  145. Tab(text: l10n.passwordLogin),
  146. Tab(text: l10n.smsLogin),
  147. ],
  148. ),
  149. ),
  150. const SizedBox(height: 24),
  151. // Tab 内容
  152. SizedBox(
  153. height: 400,
  154. child: TabBarView(
  155. controller: _tabController,
  156. children: [
  157. _buildPasswordLoginForm(),
  158. _buildSmsLoginForm(),
  159. ],
  160. ),
  161. ),
  162. ],
  163. ),
  164. ),
  165. ),
  166. ),
  167. );
  168. }
  169. // 密码登录表单
  170. Widget _buildPasswordLoginForm() {
  171. final l10n = AppLocalizations.of(context)!;
  172. return Form(
  173. key: _passwordFormKey,
  174. child: Column(
  175. crossAxisAlignment: CrossAxisAlignment.stretch,
  176. children: [
  177. const SizedBox(height: 4),
  178. // 手机号输入
  179. TextFormField(
  180. controller: _phoneController,
  181. keyboardType: TextInputType.phone,
  182. decoration: InputDecoration(
  183. labelText: l10n.phoneNumber,
  184. hintText: l10n.phoneNumberHint,
  185. prefixIcon: const Icon(Icons.phone_outlined),
  186. ),
  187. validator: (value) {
  188. if (value == null || value.isEmpty) {
  189. return l10n.phoneNumberRequired;
  190. }
  191. if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
  192. return l10n.phoneNumberInvalid;
  193. }
  194. return null;
  195. },
  196. ),
  197. const SizedBox(height: 16),
  198. // 密码输入
  199. TextFormField(
  200. controller: _passwordController,
  201. obscureText: _obscurePassword,
  202. decoration: InputDecoration(
  203. labelText: l10n.password,
  204. hintText: l10n.passwordHint,
  205. prefixIcon: const Icon(Icons.lock_outline),
  206. suffixIcon: IconButton(
  207. icon: Icon(
  208. _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
  209. ),
  210. onPressed: () {
  211. setState(() {
  212. _obscurePassword = !_obscurePassword;
  213. });
  214. },
  215. ),
  216. ),
  217. validator: (value) {
  218. if (value == null || value.isEmpty) {
  219. return l10n.passwordRequired;
  220. }
  221. if (value.length < 6) {
  222. return l10n.passwordMinLength;
  223. }
  224. return null;
  225. },
  226. ),
  227. const SizedBox(height: 8),
  228. // 忘记密码
  229. Align(
  230. alignment: Alignment.centerRight,
  231. child: TextButton(
  232. onPressed: () {
  233. // TODO: 跳转到忘记密码页面
  234. ScaffoldMessenger.of(context).showSnackBar(
  235. SnackBar(content: Text(l10n.forgotPasswordNotImplemented)),
  236. );
  237. },
  238. child: Text(l10n.forgotPassword),
  239. ),
  240. ),
  241. const SizedBox(height: 24),
  242. // 登录按钮
  243. ElevatedButton(
  244. onPressed: _handlePasswordLogin,
  245. style: ElevatedButton.styleFrom(
  246. padding: const EdgeInsets.symmetric(vertical: 16),
  247. ),
  248. child: Text(l10n.login),
  249. ),
  250. ],
  251. ),
  252. );
  253. }
  254. // 验证码登录表单
  255. Widget _buildSmsLoginForm() {
  256. final l10n = AppLocalizations.of(context)!;
  257. return Form(
  258. key: _smsFormKey,
  259. child: Column(
  260. crossAxisAlignment: CrossAxisAlignment.stretch,
  261. children: [
  262. const SizedBox(height: 4),
  263. // 手机号输入
  264. TextFormField(
  265. controller: _phoneSmsController,
  266. keyboardType: TextInputType.phone,
  267. decoration: InputDecoration(
  268. labelText: l10n.phoneNumber,
  269. hintText: l10n.phoneNumberHint,
  270. prefixIcon: const Icon(Icons.phone_outlined),
  271. ),
  272. validator: (value) {
  273. if (value == null || value.isEmpty) {
  274. return l10n.phoneNumberRequired;
  275. }
  276. if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
  277. return l10n.phoneNumberInvalid;
  278. }
  279. return null;
  280. },
  281. ),
  282. const SizedBox(height: 16),
  283. // 验证码输入
  284. Row(
  285. children: [
  286. Expanded(
  287. child: TextFormField(
  288. controller: _smsCodeController,
  289. keyboardType: TextInputType.number,
  290. decoration: InputDecoration(
  291. labelText: l10n.smsCode,
  292. hintText: l10n.smsCodeHint,
  293. prefixIcon: const Icon(Icons.sms_outlined),
  294. ),
  295. validator: (value) {
  296. if (value == null || value.isEmpty) {
  297. return l10n.smsCodeRequired;
  298. }
  299. if (value.length != 6) {
  300. return l10n.smsCodeLength;
  301. }
  302. return null;
  303. },
  304. ),
  305. ),
  306. const SizedBox(width: 12),
  307. SizedBox(
  308. width: 100,
  309. child: ElevatedButton(
  310. onPressed: _countdown > 0 ? null : _sendSmsCode,
  311. style: ElevatedButton.styleFrom(
  312. padding: const EdgeInsets.symmetric(vertical: 16),
  313. backgroundColor: _countdown > 0
  314. ? const Color(0xFFE5E7EB)
  315. : const Color(0xFF00BFA5),
  316. ),
  317. child: Text(
  318. _countdown > 0
  319. ? l10n.smsCodeCountdown(_countdown)
  320. : l10n.getSmsCode,
  321. style: TextStyle(
  322. color: _countdown > 0
  323. ? const Color(0xFF6B7280)
  324. : Colors.white,
  325. ),
  326. ),
  327. ),
  328. ),
  329. ],
  330. ),
  331. const SizedBox(height: 24),
  332. // 登录按钮
  333. ElevatedButton(
  334. onPressed: _handleSmsLogin,
  335. style: ElevatedButton.styleFrom(
  336. padding: const EdgeInsets.symmetric(vertical: 16),
  337. ),
  338. child: Text(l10n.login),
  339. ),
  340. ],
  341. ),
  342. );
  343. }
  344. }