auth_widgets.dart 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import '../../../core/l10n/app_localizations.dart';
  4. import '../../../core/theme/app_colors.dart';
  5. import '../../../providers/auth_provider.dart';
  6. // ── 输入方式 Tab(邮箱 / 手机号)────────────────────────────
  7. class AuthInputMethodTab extends StatelessWidget {
  8. const AuthInputMethodTab({
  9. super.key,
  10. required this.selected,
  11. required this.onSelect,
  12. });
  13. final AuthInputMethod selected;
  14. final ValueChanged<AuthInputMethod> onSelect;
  15. @override
  16. Widget build(BuildContext context) {
  17. return Row(
  18. children: [
  19. AuthTabItem(
  20. label: AppLocalizations.of(context)!.emailTab,
  21. active: selected == AuthInputMethod.email,
  22. onTap: () => onSelect(AuthInputMethod.email),
  23. ),
  24. const SizedBox(width: 24),
  25. AuthTabItem(
  26. label: AppLocalizations.of(context)!.phoneTab,
  27. active: selected == AuthInputMethod.phone,
  28. onTap: () => onSelect(AuthInputMethod.phone),
  29. ),
  30. ],
  31. );
  32. }
  33. }
  34. // ── 登录模式 Tab(密码 / 验证码)────────────────────────────
  35. class LoginModeTab extends StatelessWidget {
  36. const LoginModeTab({
  37. super.key,
  38. required this.selected,
  39. required this.onSelect,
  40. });
  41. final LoginMode selected;
  42. final ValueChanged<LoginMode> onSelect;
  43. @override
  44. Widget build(BuildContext context) {
  45. return Row(
  46. children: [
  47. AuthTabItem(
  48. label: AppLocalizations.of(context)!.passwordLoginTab,
  49. active: selected == LoginMode.password,
  50. onTap: () => onSelect(LoginMode.password),
  51. ),
  52. const SizedBox(width: 24),
  53. AuthTabItem(
  54. label: AppLocalizations.of(context)!.codeLoginTab,
  55. active: selected == LoginMode.code,
  56. onTap: () => onSelect(LoginMode.code),
  57. ),
  58. ],
  59. );
  60. }
  61. }
  62. // ── 单个 Tab 项 ───────────────────────────────────────────────
  63. class AuthTabItem extends StatelessWidget {
  64. const AuthTabItem({
  65. super.key,
  66. required this.label,
  67. required this.active,
  68. required this.onTap,
  69. });
  70. final String label;
  71. final bool active;
  72. final VoidCallback onTap;
  73. @override
  74. Widget build(BuildContext context) {
  75. final cs = Theme.of(context).colorScheme;
  76. return GestureDetector(
  77. onTap: onTap,
  78. child: Column(
  79. children: [
  80. Text(
  81. label,
  82. style: TextStyle(
  83. color: active ? cs.onSurface : cs.onSurface.withAlpha(153),
  84. fontSize: 15,
  85. fontWeight: active ? FontWeight.w600 : FontWeight.w400,
  86. ),
  87. ),
  88. const SizedBox(height: 4),
  89. if (active)
  90. Container(
  91. height: 2,
  92. width: 24,
  93. decoration: BoxDecoration(
  94. color: AppColors.brand,
  95. borderRadius: BorderRadius.circular(1),
  96. ),
  97. ),
  98. ],
  99. ),
  100. );
  101. }
  102. }
  103. // ── 通用输入框 ────────────────────────────────────────────────
  104. class AuthField extends StatelessWidget {
  105. const AuthField({
  106. super.key,
  107. required this.controller,
  108. required this.hint,
  109. this.obscure = false,
  110. this.suffixIcon,
  111. this.keyboardType,
  112. this.onChanged,
  113. this.maxLength,
  114. this.semanticsLabel,
  115. this.focusNode,
  116. this.errorText,
  117. this.inputFormatters,
  118. });
  119. final TextEditingController controller;
  120. final String hint;
  121. final bool obscure;
  122. final Widget? suffixIcon;
  123. final TextInputType? keyboardType;
  124. final ValueChanged<String>? onChanged;
  125. final int? maxLength;
  126. final String? semanticsLabel;
  127. final FocusNode? focusNode;
  128. final String? errorText;
  129. final List<TextInputFormatter>? inputFormatters;
  130. @override
  131. Widget build(BuildContext context) {
  132. final cs = Theme.of(context).colorScheme;
  133. final hasError = errorText != null && errorText!.isNotEmpty;
  134. final errorBorder = OutlineInputBorder(
  135. borderRadius: BorderRadius.circular(10),
  136. borderSide: BorderSide(color: cs.error, width: 1.5),
  137. );
  138. return Semantics(
  139. label: semanticsLabel,
  140. textField: true,
  141. child: TextField(
  142. controller: controller,
  143. focusNode: focusNode,
  144. obscureText: obscure,
  145. keyboardType: keyboardType,
  146. onChanged: onChanged,
  147. maxLength: maxLength,
  148. inputFormatters: inputFormatters,
  149. buildCounter: maxLength != null
  150. ? (_, {required currentLength, required isFocused, required maxLength}) => null
  151. : null,
  152. style: TextStyle(color: cs.onSurface, fontSize: 15),
  153. decoration: InputDecoration(
  154. hintText: hint,
  155. hintStyle: TextStyle(color: cs.onSurface.withAlpha(80), fontSize: 15),
  156. contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  157. suffixIcon: suffixIcon,
  158. errorText: errorText,
  159. errorStyle: TextStyle(color: cs.error, fontSize: 12),
  160. // 聚焦态:品牌黄描边,覆盖全局主题的主文字色
  161. focusedBorder: hasError
  162. ? errorBorder
  163. : OutlineInputBorder(
  164. borderRadius: BorderRadius.circular(10),
  165. borderSide: const BorderSide(color: AppColors.brand, width: 1.5),
  166. ),
  167. enabledBorder: hasError ? errorBorder : null,
  168. ),
  169. ),
  170. );
  171. }
  172. }
  173. // ── 发送验证码按钮 ────────────────────────────────────────────
  174. class SendCodeButton extends StatelessWidget {
  175. const SendCodeButton({
  176. super.key,
  177. required this.countdown,
  178. required this.onTap,
  179. });
  180. final int countdown;
  181. final VoidCallback? onTap;
  182. @override
  183. Widget build(BuildContext context) {
  184. return GestureDetector(
  185. onTap: onTap,
  186. child: Container(
  187. height: 50,
  188. padding: const EdgeInsets.symmetric(horizontal: 14),
  189. decoration: BoxDecoration(
  190. color: onTap != null ? AppColors.brand : AppColors.brand.withAlpha(60),
  191. borderRadius: BorderRadius.circular(10),
  192. ),
  193. child: Center(
  194. child: Text(
  195. countdown > 0 ? '${countdown}s' : AppLocalizations.of(context)!.sendCode,
  196. style: TextStyle(
  197. color: onTap != null ? Colors.black : Colors.black.withAlpha(100),
  198. fontSize: 13,
  199. fontWeight: FontWeight.w600,
  200. ),
  201. ),
  202. ),
  203. ),
  204. );
  205. }
  206. }