Files
xianyan/lib/features/auth/presentation/forgot_password_page.dart
Developer 8cd2703a0b chore: 汇总2026-06-08全量功能迭代与修复
此版本包含多项功能更新与问题修复:
1. 新增iOS ShareExtension分享扩展,支持多类型内容分享
2. 修复认证流程日志提示,更新用户名检测逻辑
3. 优化会话列表UI,替换emoji为CupertinoIcons原生图标
4. 修正搜索类型与频道名称映射,新增音频类型支持
5. 调整启动页布局与多语言配置
6. 重构布局约束,修复无界布局崩溃问题
7. 迁移开发者设置到更多设置页,新增日志级别配置
8. 优化TTS健康检查与自动回退逻辑
9. 新增笔记置顶会话跳转功能
10. 更新后端配置与本地化字符串
11. 重构稍后读模块,支持音频内容处理
12. 优化编辑器功能与字体管理页面
13. 新增本地数据库置顶笔记表
14. 修复Android MANAGE_STORAGE权限配置
2026-06-08 07:55:22 +08:00

976 lines
30 KiB
Dart

/// ============================================================
/// 闲言APP — 忘记密码页面
/// 创建时间: 2026-06-07
/// 更新时间: 2026-06-07
/// 作用: 未登录用户重置密码,支持密保问题/验证码/联系客服三种方式
/// 上次更新: 初始创建
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/router/app_routes.dart';
import '../../../core/router/app_nav_extension.dart';
import '../../../core/network/api_exception.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/utils/logger.dart';
import '../../../shared/widgets/feedback/app_toast.dart';
import '../../../shared/widgets/containers/glass_container.dart';
import '../../../l10n/translation_resolver.dart';
import '../../../l10n/types/t.dart';
import '../services/auth_service.dart';
import '../services/user_security_service.dart';
/// 重置方式枚举
enum _ResetMode { secQuestion, verifyCode, contactService }
class ForgotPasswordPage extends ConsumerStatefulWidget {
const ForgotPasswordPage({super.key});
@override
ConsumerState<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
}
class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
_ResetMode _currentMode = _ResetMode.secQuestion;
// ---- 密保问题方式 ----
final _accountController = TextEditingController();
final _secAnswerController = TextEditingController();
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
int? _selectedSecQuestionId;
String? _selectedSecQuestionText;
List<SecQuestionItem> _secQuestions = [];
// ---- 验证码方式 ----
final _codeAccountController = TextEditingController();
final _codeController = TextEditingController();
final _codeNewPasswordController = TextEditingController();
final _codeConfirmPasswordController = TextEditingController();
bool _codeSending = false;
int _codeCountdown = 0;
// ---- 通用 ----
bool _obscureNewPassword = true;
bool _obscureConfirmPassword = true;
bool _isSubmitting = false;
@override
void initState() {
super.initState();
_accountController.addListener(() => setState(() {}));
_secAnswerController.addListener(() => setState(() {}));
_newPasswordController.addListener(() => setState(() {}));
_confirmPasswordController.addListener(() => setState(() {}));
_codeAccountController.addListener(() => setState(() {}));
_codeController.addListener(() => setState(() {}));
_codeNewPasswordController.addListener(() => setState(() {}));
_codeConfirmPasswordController.addListener(() => setState(() {}));
_loadSecQuestions();
}
@override
void dispose() {
_accountController.dispose();
_secAnswerController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
_codeAccountController.dispose();
_codeController.dispose();
_codeNewPasswordController.dispose();
_codeConfirmPasswordController.dispose();
super.dispose();
}
// ============================================================
// 加载密保问题列表
// ============================================================
Future<void> _loadSecQuestions() async {
try {
final questions = await AuthService.secQuestions();
if (mounted) {
setState(() {
_secQuestions = questions;
});
}
} catch (e) {
Log.w('加载密保问题列表失败: $e');
}
}
// ============================================================
// 验证码倒计时
// ============================================================
void _startCountdown() {
Future.doWhile(() async {
await Future<void>.delayed(const Duration(seconds: 1));
if (!mounted) return false;
setState(() => _codeCountdown--);
return _codeCountdown > 0;
});
}
// ============================================================
// 发送验证码
// ============================================================
Future<void> _sendVerifyCode() async {
final t = ref.read(translationsProvider);
final account = _codeAccountController.text.trim();
if (account.isEmpty) {
AppToast.showWarning(t.auth.pleaseEnterAccount);
return;
}
setState(() {
_codeSending = true;
_codeCountdown = 60;
});
_startCountdown();
try {
await UserSecurityService.sendEms(
email: account,
event: 'resetpwd',
);
if (mounted) {
AppToast.showSuccess(t.auth.codeSent);
setState(() => _codeSending = false);
}
} catch (e) {
Log.e('发送验证码失败', e);
if (mounted) {
AppToast.showError(t.auth.codeSendFailedShort);
setState(() {
_codeSending = false;
_codeCountdown = 0;
});
}
}
}
// ============================================================
// 密保问题方式 — 提交重置
// ============================================================
Future<void> _handleSecQuestionReset() async {
final t = ref.read(translationsProvider);
final account = _accountController.text.trim();
final answer = _secAnswerController.text.trim();
final newPassword = _newPasswordController.text;
final confirmPassword = _confirmPasswordController.text;
// 校验
if (account.isEmpty) {
AppToast.showWarning(t.auth.pleaseEnterAccount);
return;
}
if (_selectedSecQuestionId == null) {
AppToast.showWarning(t.auth.selectSecQuestion);
return;
}
if (answer.isEmpty) {
AppToast.showWarning(t.auth.enterSecAnswerHint);
return;
}
if (newPassword.length < 6) {
AppToast.showWarning(t.auth.passwordTooShort);
return;
}
if (newPassword != confirmPassword) {
AppToast.showWarning(t.auth.passwordMismatch);
return;
}
setState(() => _isSubmitting = true);
try {
// 使用回执验证重置密码
await UserSecurityService.resetPassword(
newPassword: newPassword,
email: account,
);
if (mounted) {
AppToast.showSuccess(t.auth.resetPasswordSuccess);
_navigateBack();
}
} on ApiException catch (e) {
if (mounted) AppToast.showError(e.message);
} catch (e) {
Log.e('密保重置密码失败', e);
if (mounted) AppToast.showError('${t.auth.resetPasswordFailed}: $e');
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
// ============================================================
// 验证码方式 — 提交重置
// ============================================================
Future<void> _handleVerifyCodeReset() async {
final t = ref.read(translationsProvider);
final account = _codeAccountController.text.trim();
final code = _codeController.text.trim();
final newPassword = _codeNewPasswordController.text;
final confirmPassword = _codeConfirmPasswordController.text;
if (account.isEmpty) {
AppToast.showWarning(t.auth.pleaseEnterAccount);
return;
}
if (code.isEmpty) {
AppToast.showWarning(t.auth.pleaseEnterCode);
return;
}
if (newPassword.length < 6) {
AppToast.showWarning(t.auth.passwordTooShort);
return;
}
if (newPassword != confirmPassword) {
AppToast.showWarning(t.auth.passwordMismatch);
return;
}
setState(() => _isSubmitting = true);
try {
await UserSecurityService.resetPassword(
newPassword: newPassword,
email: account,
);
if (mounted) {
AppToast.showSuccess(t.auth.resetPasswordSuccess);
_navigateBack();
}
} on ApiException catch (e) {
if (mounted) AppToast.showError(e.message);
} catch (e) {
Log.e('验证码重置密码失败', e);
if (mounted) AppToast.showError('${t.auth.resetPasswordFailed}: $e');
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
void _navigateBack() {
if (!mounted) return;
// 重置成功后跳转登录页
context.appGo(AppRoutes.login);
}
// ============================================================
// 构建 UI
// ============================================================
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final t = ref.watch(translationsProvider);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: _buildNavLeading(ext),
middle: Text(
t.auth.forgotPasswordTitle,
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
backgroundColor: ext.bgPrimary.withValues(alpha: 0.85),
border: null,
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
child: Column(
children: [
_buildHeader(ext, t.auth),
const SizedBox(height: AppSpacing.lg),
_buildSegmentControl(ext, t.auth),
const SizedBox(height: AppSpacing.lg),
GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.lg),
child: _buildCurrentContent(ext, t.auth),
),
],
),
),
),
);
}
// ---- 导航栏返回按钮 ----
Widget _buildNavLeading(AppThemeExtension ext) {
final canPop = Navigator.of(context).canPop();
return CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {
if (canPop) {
Navigator.of(context).maybePop();
} else {
context.appGo(AppRoutes.login);
}
},
child: Icon(
canPop ? CupertinoIcons.chevron_left : CupertinoIcons.xmark,
color: ext.accent,
size: 24,
),
);
}
// ---- 页面头部 ----
Widget _buildHeader(AppThemeExtension ext, TAuth auth) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ext.accent, ext.accentLight],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: AppRadius.lgBorder,
),
child: Icon(
CupertinoIcons.lock_shield,
size: 28,
color: ext.textInverse,
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
auth.forgotPasswordTitle,
style: AppTypography.title2.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
auth.forgotPasswordSubtitle,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
),
],
),
),
],
),
);
}
// ---- 分段控制器 ----
Widget _buildSegmentControl(AppThemeExtension ext, TAuth auth) {
return SizedBox(
width: double.infinity,
height: 36,
child: CupertinoSlidingSegmentedControl<_ResetMode>(
groupValue: _currentMode,
onValueChanged: (v) {
if (v != null) setState(() => _currentMode = v);
},
children: {
_ResetMode.secQuestion: _buildSegmentItem(
ext,
CupertinoIcons.shield,
auth.resetBySecQuestion,
),
_ResetMode.verifyCode: _buildSegmentItem(
ext,
CupertinoIcons.mail,
auth.resetByVerifyCode,
),
_ResetMode.contactService: _buildSegmentItem(
ext,
CupertinoIcons.headphones,
auth.resetByContactService,
),
},
),
);
}
Widget _buildSegmentItem(
AppThemeExtension ext,
IconData icon,
String label,
) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 13, color: ext.textSecondary),
const SizedBox(width: 3),
Text(
label,
style: AppTypography.footnote.copyWith(
fontSize: 12,
color: ext.textPrimary,
),
),
],
),
);
}
// ---- 根据当前模式切换内容 ----
Widget _buildCurrentContent(AppThemeExtension ext, TAuth auth) {
return switch (_currentMode) {
_ResetMode.secQuestion => _buildSecQuestionForm(ext, auth),
_ResetMode.verifyCode => _buildVerifyCodeForm(ext, auth),
_ResetMode.contactService => _buildContactServiceContent(ext, auth),
};
}
// ============================================================
// 密保问题方式表单
// ============================================================
Widget _buildSecQuestionForm(AppThemeExtension ext, TAuth auth) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 账号输入
_buildInputField(
controller: _accountController,
placeholder: auth.accountOrEmail,
icon: CupertinoIcons.person,
ext: ext,
),
const SizedBox(height: AppSpacing.md),
// 密保问题选择
_buildSecQuestionPicker(ext, auth),
const SizedBox(height: AppSpacing.md),
// 密保答案输入
_buildInputField(
controller: _secAnswerController,
placeholder: auth.enterSecAnswerHint,
icon: CupertinoIcons.shield,
ext: ext,
),
const SizedBox(height: AppSpacing.md),
// 新密码
_buildPasswordInput(
controller: _newPasswordController,
placeholder: auth.newPasswordHint,
obscure: _obscureNewPassword,
onToggle: () =>
setState(() => _obscureNewPassword = !_obscureNewPassword),
ext: ext,
),
const SizedBox(height: AppSpacing.md),
// 确认密码
_buildPasswordInput(
controller: _confirmPasswordController,
placeholder: auth.confirmPasswordHint,
obscure: _obscureConfirmPassword,
onToggle: () => setState(
() => _obscureConfirmPassword = !_obscureConfirmPassword,
),
ext: ext,
),
const SizedBox(height: AppSpacing.lg),
// 提交按钮
_buildSubmitButton(
ext: ext,
auth: auth,
enabled: _canSubmitSecQuestion(),
onPressed: _handleSecQuestionReset,
label: auth.resetPassword,
icon: CupertinoIcons.lock_shield,
),
],
);
}
// ---- 密保问题选择器 ----
Widget _buildSecQuestionPicker(AppThemeExtension ext, TAuth auth) {
return GestureDetector(
onTap: _secQuestions.isEmpty ? null : () => _showSecQuestionPicker(ext, auth),
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: Border.all(color: ext.textHint.withValues(alpha: 0.15)),
),
child: Row(
children: [
Icon(CupertinoIcons.shield, size: 16, color: ext.textSecondary),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
_selectedSecQuestionText ?? auth.selectSecQuestion,
style: AppTypography.body.copyWith(
color: _selectedSecQuestionText != null
? ext.textPrimary
: ext.textHint,
),
),
),
Icon(
CupertinoIcons.chevron_down,
size: 14,
color: ext.textSecondary,
),
],
),
),
);
}
void _showSecQuestionPicker(AppThemeExtension ext, TAuth auth) {
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: Text(auth.selectSecQuestion),
actions: _secQuestions
.map(
(q) => CupertinoActionSheetAction(
onPressed: () {
setState(() {
_selectedSecQuestionId = q.id;
_selectedSecQuestionText = q.question;
});
Navigator.pop(ctx);
},
child: Text(
q.question,
style: AppTypography.body.copyWith(
color: _selectedSecQuestionId == q.id
? ext.accent
: ext.textPrimary,
),
),
),
)
.toList(),
cancelButton: CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(ref.read(translationsProvider).common.cancel),
),
),
);
}
bool _canSubmitSecQuestion() {
return _accountController.text.trim().isNotEmpty &&
_selectedSecQuestionId != null &&
_secAnswerController.text.trim().isNotEmpty &&
_newPasswordController.text.length >= 6 &&
_confirmPasswordController.text.length >= 6;
}
// ============================================================
// 验证码方式表单
// ============================================================
Widget _buildVerifyCodeForm(AppThemeExtension ext, TAuth auth) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 邮箱/手机号输入
_buildInputField(
controller: _codeAccountController,
placeholder: auth.resetCodeAccountHint,
icon: CupertinoIcons.mail,
ext: ext,
),
const SizedBox(height: AppSpacing.md),
// 验证码输入 + 发送按钮
Row(
children: [
Expanded(
child: _buildInputField(
controller: _codeController,
placeholder: auth.enterCodeHint,
icon: CupertinoIcons.number,
ext: ext,
),
),
const SizedBox(width: AppSpacing.sm),
SizedBox(
width: 110,
height: 44,
child: CupertinoButton(
padding: EdgeInsets.zero,
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.mdBorder,
onPressed:
(_codeCountdown <= 0 && !_codeSending && _codeAccountController.text.trim().isNotEmpty)
? _sendVerifyCode
: null,
child: Text(
_codeCountdown > 0
? '${_codeCountdown}s'
: auth.sendCode,
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
const SizedBox(height: AppSpacing.md),
// 新密码
_buildPasswordInput(
controller: _codeNewPasswordController,
placeholder: auth.newPasswordHint,
obscure: _obscureNewPassword,
onToggle: () =>
setState(() => _obscureNewPassword = !_obscureNewPassword),
ext: ext,
),
const SizedBox(height: AppSpacing.md),
// 确认密码
_buildPasswordInput(
controller: _codeConfirmPasswordController,
placeholder: auth.confirmPasswordHint,
obscure: _obscureConfirmPassword,
onToggle: () => setState(
() => _obscureConfirmPassword = !_obscureConfirmPassword,
),
ext: ext,
),
const SizedBox(height: AppSpacing.lg),
// 提交按钮
_buildSubmitButton(
ext: ext,
auth: auth,
enabled: _canSubmitVerifyCode(),
onPressed: _handleVerifyCodeReset,
label: auth.resetPassword,
icon: CupertinoIcons.lock_shield,
),
],
);
}
bool _canSubmitVerifyCode() {
return _codeAccountController.text.trim().isNotEmpty &&
_codeController.text.trim().isNotEmpty &&
_codeNewPasswordController.text.length >= 6 &&
_codeConfirmPasswordController.text.length >= 6;
}
// ============================================================
// 联系客服内容
// ============================================================
Widget _buildContactServiceContent(AppThemeExtension ext, TAuth auth) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 提示图标和标题
Center(
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.lgBorder,
),
child: Icon(
CupertinoIcons.headphones,
size: 24,
color: ext.accent,
),
),
),
const SizedBox(height: AppSpacing.md),
Center(
child: Text(
auth.contactServiceTitle,
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: AppSpacing.sm),
Center(
child: Text(
auth.contactServiceSubtitle,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: AppSpacing.lg),
// 需要提供的信息列表
_buildInfoItem(
ext,
icon: CupertinoIcons.person,
label: auth.contactServiceInfoAccount,
),
const SizedBox(height: AppSpacing.sm),
_buildInfoItem(
ext,
icon: CupertinoIcons.mail,
label: auth.contactServiceInfoEmail,
),
const SizedBox(height: AppSpacing.sm),
_buildInfoItem(
ext,
icon: CupertinoIcons.device_phone_portrait,
label: auth.contactServiceInfoDevice,
),
const SizedBox(height: AppSpacing.sm),
_buildInfoItem(
ext,
icon: CupertinoIcons.doc_text,
label: auth.contactServiceInfoDescription,
),
const SizedBox(height: AppSpacing.lg),
// 联系方式
GlassContainer(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
CupertinoIcons.link,
size: 16,
color: ext.accent,
),
const SizedBox(width: AppSpacing.sm),
Text(
auth.contactServiceMethod,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
auth.contactServiceMethodDetail,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
],
),
),
const SizedBox(height: AppSpacing.lg),
// 返回登录按钮
SizedBox(
width: double.infinity,
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.accent,
borderRadius: AppRadius.mdBorder,
onPressed: () => context.appGo(AppRoutes.login),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.arrow_left, size: 16, color: ext.textOnAccent),
const SizedBox(width: 6),
Text(
auth.goLogin,
style: AppTypography.callout.copyWith(
color: ext.textOnAccent,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
);
}
Widget _buildInfoItem(AppThemeExtension ext, {
required IconData icon,
required String label,
}) {
return Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.08),
borderRadius: AppRadius.smBorder,
),
child: Icon(icon, size: 14, color: ext.accent),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
label,
style: AppTypography.body.copyWith(color: ext.textPrimary),
),
),
],
);
}
// ============================================================
// 通用输入框
// ============================================================
Widget _buildInputField({
required TextEditingController controller,
required String placeholder,
required IconData icon,
required AppThemeExtension ext,
bool obscure = false,
}) {
return SizedBox(
height: 44,
child: CupertinoTextField(
controller: controller,
placeholder: placeholder,
obscureText: obscure,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: Border.all(color: ext.textHint.withValues(alpha: 0.15)),
),
style: AppTypography.body.copyWith(color: ext.textPrimary),
placeholderStyle: AppTypography.body.copyWith(color: ext.textHint),
prefix: Padding(
padding: const EdgeInsets.only(left: AppSpacing.md),
child: Icon(icon, size: 16, color: ext.textSecondary),
),
),
);
}
Widget _buildPasswordInput({
required TextEditingController controller,
required String placeholder,
required bool obscure,
required VoidCallback onToggle,
required AppThemeExtension ext,
}) {
return SizedBox(
height: 44,
child: CupertinoTextField(
controller: controller,
placeholder: placeholder,
obscureText: obscure,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: Border.all(color: ext.textHint.withValues(alpha: 0.15)),
),
style: AppTypography.body.copyWith(color: ext.textPrimary),
placeholderStyle: AppTypography.body.copyWith(color: ext.textHint),
prefix: Padding(
padding: const EdgeInsets.only(left: AppSpacing.md),
child: Icon(CupertinoIcons.lock, size: 16, color: ext.textSecondary),
),
suffix: GestureDetector(
onTap: onToggle,
child: Padding(
padding: const EdgeInsets.only(right: AppSpacing.md),
child: Icon(
obscure ? CupertinoIcons.eye_slash : CupertinoIcons.eye,
size: 16,
color: ext.textSecondary,
),
),
),
),
);
}
// ============================================================
// 提交按钮
// ============================================================
Widget _buildSubmitButton({
required AppThemeExtension ext,
required TAuth auth,
required bool enabled,
required VoidCallback onPressed,
required String label,
required IconData icon,
}) {
return SizedBox(
width: double.infinity,
height: 50,
child: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
color: ext.accent,
borderRadius: AppRadius.mdBorder,
onPressed: (enabled && !_isSubmitting) ? onPressed : null,
child: _isSubmitting
? CupertinoActivityIndicator(color: ext.textOnAccent)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: ext.textOnAccent),
const SizedBox(width: 6),
Text(
label,
style: AppTypography.callout.copyWith(
color: ext.textOnAccent,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}