此版本包含多项功能更新与问题修复: 1. 新增iOS ShareExtension分享扩展,支持多类型内容分享 2. 修复认证流程日志提示,更新用户名检测逻辑 3. 优化会话列表UI,替换emoji为CupertinoIcons原生图标 4. 修正搜索类型与频道名称映射,新增音频类型支持 5. 调整启动页布局与多语言配置 6. 重构布局约束,修复无界布局崩溃问题 7. 迁移开发者设置到更多设置页,新增日志级别配置 8. 优化TTS健康检查与自动回退逻辑 9. 新增笔记置顶会话跳转功能 10. 更新后端配置与本地化字符串 11. 重构稍后读模块,支持音频内容处理 12. 优化编辑器功能与字体管理页面 13. 新增本地数据库置顶笔记表 14. 修复Android MANAGE_STORAGE权限配置
976 lines
30 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|