fix(referral): correct response parsing for authorization-service API
The authorization-service uses TransformInterceptor which wraps all API
responses in a standard format: { success, data, timestamp }
Before this fix, the AuthorizationServiceClient was reading:
response.data.accountSequence (undefined)
After this fix, it correctly reads:
response.data.data.accountSequence
This ensures that nearestCommunity, nearestProvinceAuth, and nearestCityAuth
are properly extracted from the wrapped response, allowing community benefits
to be correctly allocated to authorized users instead of falling back to
SYSTEM_HEADQUARTERS_COMMUNITY.
Changes:
- Added AuthorizationServiceResponse<T> interface to model wrapped response
- Updated findNearestCommunity() to use wrapped response type
- Updated findNearestProvince() to use wrapped response type
- Updated findNearestCity() to use wrapped response type
- Updated catchError handlers to return properly structured fallback data
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ee9265f357
commit
d1a7c44f23
|
|
@ -4,6 +4,13 @@ import { HttpService } from '@nestjs/axios';
|
|||
import { firstValueFrom, timeout, catchError } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
// authorization-service 返回格式(经过 TransformInterceptor 包装)
|
||||
export interface AuthorizationServiceResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface NearestAuthorizationResult {
|
||||
accountSequence: number | null;
|
||||
}
|
||||
|
|
@ -37,7 +44,7 @@ export class AuthorizationServiceClient {
|
|||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService
|
||||
.get<NearestAuthorizationResult>(
|
||||
.get<AuthorizationServiceResponse<NearestAuthorizationResult>>(
|
||||
`${this.baseUrl}/api/v1/authorization/nearest-community`,
|
||||
{
|
||||
params: { accountSequence },
|
||||
|
|
@ -49,12 +56,13 @@ export class AuthorizationServiceClient {
|
|||
this.logger.warn(
|
||||
`Failed to find nearest community for accountSequence=${accountSequence}: ${error.message}`,
|
||||
);
|
||||
return of({ data: { accountSequence: null } });
|
||||
return of({ data: { success: false, data: { accountSequence: null }, timestamp: '' } });
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return response.data.accountSequence;
|
||||
// authorization-service 返回格式: { success, data: { accountSequence }, timestamp }
|
||||
return response.data.data?.accountSequence ?? null;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error finding nearest community for accountSequence=${accountSequence}`,
|
||||
|
|
@ -77,7 +85,7 @@ export class AuthorizationServiceClient {
|
|||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService
|
||||
.get<NearestAuthorizationResult>(
|
||||
.get<AuthorizationServiceResponse<NearestAuthorizationResult>>(
|
||||
`${this.baseUrl}/api/v1/authorization/nearest-province`,
|
||||
{
|
||||
params: { accountSequence, provinceCode },
|
||||
|
|
@ -89,12 +97,13 @@ export class AuthorizationServiceClient {
|
|||
this.logger.warn(
|
||||
`Failed to find nearest province for accountSequence=${accountSequence}, provinceCode=${provinceCode}: ${error.message}`,
|
||||
);
|
||||
return of({ data: { accountSequence: null } });
|
||||
return of({ data: { success: false, data: { accountSequence: null }, timestamp: '' } });
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return response.data.accountSequence;
|
||||
// authorization-service 返回格式: { success, data: { accountSequence }, timestamp }
|
||||
return response.data.data?.accountSequence ?? null;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error finding nearest province for accountSequence=${accountSequence}, provinceCode=${provinceCode}`,
|
||||
|
|
@ -117,7 +126,7 @@ export class AuthorizationServiceClient {
|
|||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService
|
||||
.get<NearestAuthorizationResult>(
|
||||
.get<AuthorizationServiceResponse<NearestAuthorizationResult>>(
|
||||
`${this.baseUrl}/api/v1/authorization/nearest-city`,
|
||||
{
|
||||
params: { accountSequence, cityCode },
|
||||
|
|
@ -129,12 +138,13 @@ export class AuthorizationServiceClient {
|
|||
this.logger.warn(
|
||||
`Failed to find nearest city for accountSequence=${accountSequence}, cityCode=${cityCode}: ${error.message}`,
|
||||
);
|
||||
return of({ data: { accountSequence: null } });
|
||||
return of({ data: { success: false, data: { accountSequence: null }, timestamp: '' } });
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return response.data.accountSequence;
|
||||
// authorization-service 返回格式: { success, data: { accountSequence }, timestamp }
|
||||
return response.data.data?.accountSequence ?? null;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error finding nearest city for accountSequence=${accountSequence}, cityCode=${cityCode}`,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ class PlantingLocationPage extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
||||
/// 本地存储 key 前缀
|
||||
static const String _keyProvinceName = 'planting_province_name';
|
||||
static const String _keyProvinceCode = 'planting_province_code';
|
||||
static const String _keyCityName = 'planting_city_name';
|
||||
static const String _keyCityCode = 'planting_city_code';
|
||||
|
||||
/// 选中的省份名称
|
||||
String? _selectedProvinceName;
|
||||
|
||||
|
|
@ -53,6 +59,59 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
|||
/// 是否正在提交
|
||||
bool _isSubmitting = false;
|
||||
|
||||
/// 是否有保存的省市记录(用于判断是否跳过倒计时)
|
||||
bool _hasSavedLocation = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSavedLocation();
|
||||
}
|
||||
|
||||
/// 从本地存储加载保存的省市
|
||||
void _loadSavedLocation() {
|
||||
try {
|
||||
final localStorage = ref.read(localStorageProvider);
|
||||
final provinceName = localStorage.getString(_keyProvinceName);
|
||||
final provinceCode = localStorage.getString(_keyProvinceCode);
|
||||
final cityName = localStorage.getString(_keyCityName);
|
||||
final cityCode = localStorage.getString(_keyCityCode);
|
||||
|
||||
if (provinceName != null && provinceCode != null &&
|
||||
cityName != null && cityCode != null) {
|
||||
setState(() {
|
||||
_selectedProvinceName = provinceName;
|
||||
_selectedProvinceCode = provinceCode;
|
||||
_selectedCityName = cityName;
|
||||
_selectedCityCode = cityCode;
|
||||
_hasSavedLocation = true;
|
||||
});
|
||||
debugPrint('[PlantingLocationPage] 已加载保存的省市: $provinceName · $cityName');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[PlantingLocationPage] 加载省市失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存省市到本地存储
|
||||
Future<void> _saveLocation() async {
|
||||
if (_selectedProvinceName == null || _selectedProvinceCode == null ||
|
||||
_selectedCityName == null || _selectedCityCode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final localStorage = ref.read(localStorageProvider);
|
||||
await localStorage.setString(_keyProvinceName, _selectedProvinceName!);
|
||||
await localStorage.setString(_keyProvinceCode, _selectedProvinceCode!);
|
||||
await localStorage.setString(_keyCityName, _selectedCityName!);
|
||||
await localStorage.setString(_keyCityCode, _selectedCityCode!);
|
||||
debugPrint('[PlantingLocationPage] 已保存省市: $_selectedProvinceName · $_selectedCityName');
|
||||
} catch (e) {
|
||||
debugPrint('[PlantingLocationPage] 保存省市失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回上一页
|
||||
void _goBack() {
|
||||
context.pop();
|
||||
|
|
@ -107,11 +166,17 @@ class _PlantingLocationPageState extends ConsumerState<PlantingLocationPage> {
|
|||
return;
|
||||
}
|
||||
|
||||
// 显示确认弹窗(带5秒倒计时)
|
||||
// 保存省市到本地(首次选择时保存)
|
||||
if (!_hasSavedLocation) {
|
||||
await _saveLocation();
|
||||
}
|
||||
|
||||
// 显示确认弹窗(如果有保存记录则跳过倒计时)
|
||||
await PlantingConfirmDialog.show(
|
||||
context: context,
|
||||
province: _selectedProvinceName!,
|
||||
city: _selectedCityName!,
|
||||
skipCountdown: _hasSavedLocation,
|
||||
onConfirm: _submitPlanting,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,15 @@ class PlantingConfirmDialog extends StatefulWidget {
|
|||
/// 确认回调
|
||||
final VoidCallback onConfirm;
|
||||
|
||||
/// 是否跳过倒计时(用户之前已选择过省市)
|
||||
final bool skipCountdown;
|
||||
|
||||
const PlantingConfirmDialog({
|
||||
super.key,
|
||||
required this.province,
|
||||
required this.city,
|
||||
required this.onConfirm,
|
||||
this.skipCountdown = false,
|
||||
});
|
||||
|
||||
/// 显示确认弹窗
|
||||
|
|
@ -26,6 +30,7 @@ class PlantingConfirmDialog extends StatefulWidget {
|
|||
required String province,
|
||||
required String city,
|
||||
required VoidCallback onConfirm,
|
||||
bool skipCountdown = false,
|
||||
}) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
|
|
@ -35,6 +40,7 @@ class PlantingConfirmDialog extends StatefulWidget {
|
|||
province: province,
|
||||
city: city,
|
||||
onConfirm: onConfirm,
|
||||
skipCountdown: skipCountdown,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -45,7 +51,7 @@ class PlantingConfirmDialog extends StatefulWidget {
|
|||
|
||||
class _PlantingConfirmDialogState extends State<PlantingConfirmDialog> {
|
||||
/// 倒计时秒数
|
||||
int _countdown = 5;
|
||||
late int _countdown;
|
||||
|
||||
/// 倒计时定时器
|
||||
Timer? _timer;
|
||||
|
|
@ -56,7 +62,11 @@ class _PlantingConfirmDialogState extends State<PlantingConfirmDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startCountdown();
|
||||
// 如果跳过倒计时,直接设置为0,否则从5开始
|
||||
_countdown = widget.skipCountdown ? 0 : 5;
|
||||
if (!widget.skipCountdown) {
|
||||
_startCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,787 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// 绑定邮箱页面
|
||||
/// 支持绑定/更换邮箱,需要验证码验证
|
||||
class BindEmailPage extends ConsumerStatefulWidget {
|
||||
const BindEmailPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<BindEmailPage> createState() => _BindEmailPageState();
|
||||
}
|
||||
|
||||
class _BindEmailPageState extends ConsumerState<BindEmailPage> {
|
||||
/// 邮箱控制器
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
|
||||
/// 验证码控制器
|
||||
final TextEditingController _codeController = TextEditingController();
|
||||
|
||||
/// 是否正在加载
|
||||
bool _isLoading = true;
|
||||
|
||||
/// 是否已绑定邮箱
|
||||
bool _isBound = false;
|
||||
|
||||
/// 当前绑定的邮箱(脱敏显示)
|
||||
String? _currentEmail;
|
||||
|
||||
/// 是否正在发送验证码
|
||||
bool _isSendingCode = false;
|
||||
|
||||
/// 验证码发送倒计时
|
||||
int _countdown = 0;
|
||||
|
||||
/// 倒计时定时器
|
||||
Timer? _countdownTimer;
|
||||
|
||||
/// 是否正在提交
|
||||
bool _isSubmitting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEmailStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_codeController.dispose();
|
||||
_countdownTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 加载邮箱状态
|
||||
Future<void> _loadEmailStatus() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API获取邮箱绑定状态
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// final emailStatus = await accountService.getEmailStatus();
|
||||
|
||||
// 模拟数据
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isBound = false; // 模拟未绑定
|
||||
_currentEmail = null;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回上一页
|
||||
void _goBack() {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
/// 验证邮箱格式
|
||||
bool _isValidEmail(String email) {
|
||||
return RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$').hasMatch(email);
|
||||
}
|
||||
|
||||
/// 发送验证码
|
||||
Future<void> _sendVerificationCode() async {
|
||||
final email = _emailController.text.trim();
|
||||
|
||||
if (email.isEmpty) {
|
||||
_showErrorSnackBar('请输入邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isValidEmail(email)) {
|
||||
_showErrorSnackBar('请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSendingCode = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API发送验证码
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// await accountService.sendEmailCode(email);
|
||||
|
||||
// 模拟请求
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSendingCode = false;
|
||||
_countdown = 60;
|
||||
});
|
||||
|
||||
// 启动倒计时
|
||||
_startCountdown();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('验证码已发送'),
|
||||
backgroundColor: Color(0xFFD4AF37),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSendingCode = false;
|
||||
});
|
||||
|
||||
_showErrorSnackBar('发送失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动倒计时
|
||||
void _startCountdown() {
|
||||
_countdownTimer?.cancel();
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_countdown > 0) {
|
||||
setState(() {
|
||||
_countdown--;
|
||||
});
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 绑定邮箱
|
||||
Future<void> _bindEmail() async {
|
||||
final email = _emailController.text.trim();
|
||||
final code = _codeController.text.trim();
|
||||
|
||||
if (email.isEmpty) {
|
||||
_showErrorSnackBar('请输入邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isValidEmail(email)) {
|
||||
_showErrorSnackBar('请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.isEmpty) {
|
||||
_showErrorSnackBar('请输入验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length != 6 || !RegExp(r'^\d{6}$').hasMatch(code)) {
|
||||
_showErrorSnackBar('验证码格式错误,请输入6位数字');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API绑定邮箱
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// await accountService.bindEmail(email, code);
|
||||
|
||||
// 模拟请求
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_isBound ? '邮箱更换成功' : '邮箱绑定成功'),
|
||||
backgroundColor: const Color(0xFF4CAF50),
|
||||
),
|
||||
);
|
||||
|
||||
context.pop(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
_showErrorSnackBar('操作失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解绑邮箱
|
||||
Future<void> _unbindEmail() async {
|
||||
final code = _codeController.text.trim();
|
||||
|
||||
if (code.isEmpty) {
|
||||
_showErrorSnackBar('请输入验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: const Text(
|
||||
'确认解绑',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
content: const Text(
|
||||
'解绑后将无法通过该邮箱找回账户,确定要解绑吗?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text(
|
||||
'确认解绑',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API解绑邮箱
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// await accountService.unbindEmail(code);
|
||||
|
||||
// 模拟请求
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isBound = false;
|
||||
_currentEmail = null;
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('邮箱已解绑'),
|
||||
backgroundColor: Color(0xFFD4AF37),
|
||||
),
|
||||
);
|
||||
|
||||
// 清空输入
|
||||
_emailController.clear();
|
||||
_codeController.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
_showErrorSnackBar('解绑失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示错误提示
|
||||
void _showErrorSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFF5E6),
|
||||
Color(0xFFFFE4B5),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
_buildAppBar(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态卡片
|
||||
_buildStatusCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 邮箱输入
|
||||
_buildEmailInput(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 验证码输入
|
||||
_buildCodeInput(),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 提示信息
|
||||
_buildHintText(),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 提交按钮
|
||||
_buildSubmitButton(),
|
||||
|
||||
// 解绑按钮(已绑定时显示)
|
||||
if (_isBound) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildUnbindButton(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部导航栏
|
||||
Widget _buildAppBar() {
|
||||
return Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
GestureDetector(
|
||||
onTap: _goBack,
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.arrow_back,
|
||||
size: 24,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 标题
|
||||
Expanded(
|
||||
child: Text(
|
||||
_isBound ? '更换邮箱' : '绑定邮箱',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.25,
|
||||
letterSpacing: -0.27,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
// 占位
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态卡片
|
||||
Widget _buildStatusCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: _isBound
|
||||
? const Color(0xFF4CAF50).withValues(alpha: 0.1)
|
||||
: const Color(0xFFFF9800).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(
|
||||
_isBound ? Icons.email : Icons.email_outlined,
|
||||
color: _isBound ? const Color(0xFF4CAF50) : const Color(0xFFFF9800),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_isBound ? '已绑定' : '未绑定',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _isBound ? const Color(0xFF4CAF50) : const Color(0xFFFF9800),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_isBound
|
||||
? '当前邮箱: $_currentEmail'
|
||||
: '绑定邮箱后可用于账户安全验证',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建邮箱输入框
|
||||
Widget _buildEmailInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_isBound ? '新邮箱地址' : '邮箱地址',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 54,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x80FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.19,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
border: InputBorder.none,
|
||||
hintText: '请输入邮箱地址',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.19,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.email_outlined,
|
||||
color: Color(0xFF8B5A2B),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建验证码输入框
|
||||
Widget _buildCodeInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'验证码',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
// 验证码输入框
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 54,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x80FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.19,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
border: InputBorder.none,
|
||||
hintText: '请输入验证码',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.19,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 发送验证码按钮
|
||||
GestureDetector(
|
||||
onTap: (_countdown > 0 || _isSendingCode) ? null : _sendVerificationCode,
|
||||
child: Container(
|
||||
height: 54,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: (_countdown > 0 || _isSendingCode)
|
||||
? const Color(0xFFD4AF37).withValues(alpha: 0.5)
|
||||
: const Color(0xFFD4AF37),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: _isSendingCode
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_countdown > 0 ? '${_countdown}s' : '获取验证码',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提示文字
|
||||
Widget _buildHintText() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5E6),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'验证码将发送到您输入的邮箱,请注意查收。如未收到,请检查垃圾邮件。',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提交按钮
|
||||
Widget _buildSubmitButton() {
|
||||
return GestureDetector(
|
||||
onTap: _isSubmitting ? null : _bindEmail,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: _isSubmitting
|
||||
? const Color(0xFFD4AF37).withValues(alpha: 0.5)
|
||||
: const Color(0xFFD4AF37),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x4DD4AF37),
|
||||
blurRadius: 14,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_isBound ? '更换邮箱' : '绑定邮箱',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.5,
|
||||
letterSpacing: 0.24,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建解绑按钮
|
||||
Widget _buildUnbindButton() {
|
||||
return GestureDetector(
|
||||
onTap: _isSubmitting ? null : _unbindEmail,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.red.withValues(alpha: 0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'解绑邮箱',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.5,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,583 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// 修改密码页面
|
||||
/// 支持设置或修改登录密码
|
||||
class ChangePasswordPage extends ConsumerStatefulWidget {
|
||||
const ChangePasswordPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ChangePasswordPage> createState() => _ChangePasswordPageState();
|
||||
}
|
||||
|
||||
class _ChangePasswordPageState extends ConsumerState<ChangePasswordPage> {
|
||||
/// 旧密码控制器
|
||||
final TextEditingController _oldPasswordController = TextEditingController();
|
||||
|
||||
/// 新密码控制器
|
||||
final TextEditingController _newPasswordController = TextEditingController();
|
||||
|
||||
/// 确认密码控制器
|
||||
final TextEditingController _confirmPasswordController = TextEditingController();
|
||||
|
||||
/// 是否显示旧密码
|
||||
bool _showOldPassword = false;
|
||||
|
||||
/// 是否显示新密码
|
||||
bool _showNewPassword = false;
|
||||
|
||||
/// 是否显示确认密码
|
||||
bool _showConfirmPassword = false;
|
||||
|
||||
/// 是否正在提交
|
||||
bool _isSubmitting = false;
|
||||
|
||||
/// 是否已设置过密码
|
||||
bool _hasPassword = false;
|
||||
|
||||
/// 是否正在加载
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPasswordStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_oldPasswordController.dispose();
|
||||
_newPasswordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 加载密码状态
|
||||
Future<void> _loadPasswordStatus() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API获取密码状态
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// final hasPassword = await accountService.hasPassword();
|
||||
|
||||
// 模拟数据
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasPassword = false; // 模拟未设置密码
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回上一页
|
||||
void _goBack() {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
/// 验证密码强度
|
||||
String? _validatePasswordStrength(String password) {
|
||||
if (password.length < 8) {
|
||||
return '密码长度至少8位';
|
||||
}
|
||||
if (password.length > 20) {
|
||||
return '密码长度不能超过20位';
|
||||
}
|
||||
if (!RegExp(r'[A-Za-z]').hasMatch(password)) {
|
||||
return '密码必须包含字母';
|
||||
}
|
||||
if (!RegExp(r'[0-9]').hasMatch(password)) {
|
||||
return '密码必须包含数字';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 提交修改密码
|
||||
Future<void> _submitPassword() async {
|
||||
final oldPassword = _oldPasswordController.text;
|
||||
final newPassword = _newPasswordController.text;
|
||||
final confirmPassword = _confirmPasswordController.text;
|
||||
|
||||
// 验证旧密码(如果已设置过密码)
|
||||
if (_hasPassword && oldPassword.isEmpty) {
|
||||
_showErrorSnackBar('请输入当前密码');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证新密码
|
||||
if (newPassword.isEmpty) {
|
||||
_showErrorSnackBar('请输入新密码');
|
||||
return;
|
||||
}
|
||||
|
||||
final strengthError = _validatePasswordStrength(newPassword);
|
||||
if (strengthError != null) {
|
||||
_showErrorSnackBar(strengthError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证确认密码
|
||||
if (confirmPassword.isEmpty) {
|
||||
_showErrorSnackBar('请确认新密码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword != confirmPassword) {
|
||||
_showErrorSnackBar('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API修改密码
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// await accountService.changePassword(
|
||||
// oldPassword: _hasPassword ? oldPassword : null,
|
||||
// newPassword: newPassword,
|
||||
// );
|
||||
|
||||
// 模拟请求
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_hasPassword ? '密码修改成功' : '密码设置成功'),
|
||||
backgroundColor: const Color(0xFF4CAF50),
|
||||
),
|
||||
);
|
||||
|
||||
context.pop(true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
_showErrorSnackBar('操作失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示错误提示
|
||||
void _showErrorSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFF5E6),
|
||||
Color(0xFFFFE4B5),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
_buildAppBar(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态提示
|
||||
_buildStatusCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 密码强度要求
|
||||
_buildPasswordRequirements(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 旧密码输入(仅在已设置密码时显示)
|
||||
if (_hasPassword) ...[
|
||||
_buildPasswordInput(
|
||||
controller: _oldPasswordController,
|
||||
label: '当前密码',
|
||||
hint: '请输入当前密码',
|
||||
showPassword: _showOldPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showOldPassword = !_showOldPassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// 新密码输入
|
||||
_buildPasswordInput(
|
||||
controller: _newPasswordController,
|
||||
label: '新密码',
|
||||
hint: '请输入新密码',
|
||||
showPassword: _showNewPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showNewPassword = !_showNewPassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 确认密码输入
|
||||
_buildPasswordInput(
|
||||
controller: _confirmPasswordController,
|
||||
label: '确认密码',
|
||||
hint: '请再次输入新密码',
|
||||
showPassword: _showConfirmPassword,
|
||||
onToggleVisibility: () {
|
||||
setState(() {
|
||||
_showConfirmPassword = !_showConfirmPassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 提交按钮
|
||||
_buildSubmitButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部导航栏
|
||||
Widget _buildAppBar() {
|
||||
return Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
GestureDetector(
|
||||
onTap: _goBack,
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.arrow_back,
|
||||
size: 24,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 标题
|
||||
Expanded(
|
||||
child: Text(
|
||||
_hasPassword ? '修改密码' : '设置密码',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.25,
|
||||
letterSpacing: -0.27,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
// 占位
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态卡片
|
||||
Widget _buildStatusCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: _hasPassword
|
||||
? const Color(0xFF4CAF50).withValues(alpha: 0.1)
|
||||
: const Color(0xFFFF9800).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(
|
||||
_hasPassword ? Icons.lock : Icons.lock_open,
|
||||
color: _hasPassword ? const Color(0xFF4CAF50) : const Color(0xFFFF9800),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_hasPassword ? '已设置密码' : '未设置密码',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _hasPassword ? const Color(0xFF4CAF50) : const Color(0xFFFF9800),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_hasPassword ? '您可以修改当前登录密码' : '设置密码后可使用密码登录',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建密码要求提示
|
||||
Widget _buildPasswordRequirements() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5E6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: const [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'密码要求',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRequirementItem('长度为 8-20 个字符'),
|
||||
_buildRequirementItem('必须包含字母'),
|
||||
_buildRequirementItem('必须包含数字'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建要求项
|
||||
Widget _buildRequirementItem(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 16,
|
||||
color: Color(0xFFD4AF37),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建密码输入框
|
||||
Widget _buildPasswordInput({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
required bool showPassword,
|
||||
required VoidCallback onToggleVisibility,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 54,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x80FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
obscureText: !showPassword,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.19,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
border: InputBorder.none,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.19,
|
||||
color: Color(0x995D4037),
|
||||
),
|
||||
suffixIcon: GestureDetector(
|
||||
onTap: onToggleVisibility,
|
||||
child: Icon(
|
||||
showPassword ? Icons.visibility_off : Icons.visibility,
|
||||
color: const Color(0xFF8B5A2B),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提交按钮
|
||||
Widget _buildSubmitButton() {
|
||||
return GestureDetector(
|
||||
onTap: _isSubmitting ? null : _submitPassword,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: _isSubmitting
|
||||
? const Color(0xFFD4AF37).withValues(alpha: 0.5)
|
||||
: const Color(0xFFD4AF37),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x4DD4AF37),
|
||||
blurRadius: 14,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_hasPassword ? '修改密码' : '设置密码',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.5,
|
||||
letterSpacing: 0.24,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,886 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
/// 谷歌验证器页面
|
||||
/// 用于绑定/解绑谷歌验证器,提供二维码扫描和密钥复制功能
|
||||
class GoogleAuthPage extends ConsumerStatefulWidget {
|
||||
const GoogleAuthPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<GoogleAuthPage> createState() => _GoogleAuthPageState();
|
||||
}
|
||||
|
||||
class _GoogleAuthPageState extends ConsumerState<GoogleAuthPage> {
|
||||
/// 验证码输入控制器
|
||||
final TextEditingController _codeController = TextEditingController();
|
||||
|
||||
/// 是否正在加载
|
||||
bool _isLoading = true;
|
||||
|
||||
/// 是否已绑定谷歌验证器
|
||||
bool _isBound = false;
|
||||
|
||||
/// 谷歌验证器密钥(用于生成二维码)
|
||||
String? _secret;
|
||||
|
||||
/// 谷歌验证器 URI(用于生成二维码)
|
||||
String? _otpAuthUri;
|
||||
|
||||
/// 是否正在提交
|
||||
bool _isSubmitting = false;
|
||||
|
||||
/// 错误信息
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGoogleAuthStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 加载谷歌验证器状态
|
||||
Future<void> _loadGoogleAuthStatus() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API获取谷歌验证器状态
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// final status = await accountService.getGoogleAuthStatus();
|
||||
|
||||
// 模拟数据
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isBound = false; // 模拟未绑定状态
|
||||
_secret = 'JBSWY3DPEHPK3PXP'; // 模拟密钥
|
||||
_otpAuthUri = 'otpauth://totp/RWADurian:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=RWADurian';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = '加载失败: ${e.toString()}';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回上一页
|
||||
void _goBack() {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
/// 复制密钥到剪贴板
|
||||
void _copySecret() {
|
||||
if (_secret != null) {
|
||||
Clipboard.setData(ClipboardData(text: _secret!));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('密钥已复制到剪贴板'),
|
||||
backgroundColor: Color(0xFFD4AF37),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 绑定谷歌验证器
|
||||
Future<void> _bindGoogleAuth() async {
|
||||
final code = _codeController.text.trim();
|
||||
|
||||
if (code.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请输入验证码'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length != 6 || !RegExp(r'^\d{6}$').hasMatch(code)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('验证码格式错误,请输入6位数字'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API绑定谷歌验证器
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// await accountService.bindGoogleAuth(code);
|
||||
|
||||
// 模拟请求
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isBound = true;
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('谷歌验证器绑定成功'),
|
||||
backgroundColor: Color(0xFF4CAF50),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('绑定失败: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解绑谷歌验证器
|
||||
Future<void> _unbindGoogleAuth() async {
|
||||
final code = _codeController.text.trim();
|
||||
|
||||
if (code.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请输入验证码'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: const Text(
|
||||
'确认解绑',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
content: const Text(
|
||||
'解绑后将降低账户安全性,确定要解绑谷歌验证器吗?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text(
|
||||
'确认解绑',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: 调用API解绑谷歌验证器
|
||||
// final accountService = ref.read(accountServiceProvider);
|
||||
// await accountService.unbindGoogleAuth(code);
|
||||
|
||||
// 模拟请求
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isBound = false;
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('谷歌验证器已解绑'),
|
||||
backgroundColor: Color(0xFFD4AF37),
|
||||
),
|
||||
);
|
||||
|
||||
// 重新加载状态(获取新的密钥)
|
||||
_loadGoogleAuthStatus();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('解绑失败: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFF5E6),
|
||||
Color(0xFFFFE4B5),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
_buildAppBar(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||
),
|
||||
)
|
||||
: _errorMessage != null
|
||||
? _buildErrorView()
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _isBound
|
||||
? _buildBoundContent()
|
||||
: _buildUnboundContent(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建顶部导航栏
|
||||
Widget _buildAppBar() {
|
||||
return Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
GestureDetector(
|
||||
onTap: _goBack,
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.arrow_back,
|
||||
size: 24,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 标题
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'谷歌验证器',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.25,
|
||||
letterSpacing: -0.27,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
// 占位
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建错误视图
|
||||
Widget _buildErrorView() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
GestureDetector(
|
||||
onTap: _loadGoogleAuthStatus,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD4AF37),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'重试',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建未绑定状态内容
|
||||
Widget _buildUnboundContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态提示
|
||||
_buildStatusCard(
|
||||
icon: Icons.security,
|
||||
title: '未绑定',
|
||||
subtitle: '绑定谷歌验证器可提升账户安全性',
|
||||
color: const Color(0xFFFF9800),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 步骤说明
|
||||
_buildStepCard(
|
||||
stepNumber: '1',
|
||||
title: '下载谷歌验证器',
|
||||
description: '在应用商店搜索 "Google Authenticator" 并下载安装',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildStepCard(
|
||||
stepNumber: '2',
|
||||
title: '扫描二维码或输入密钥',
|
||||
description: '使用谷歌验证器扫描下方二维码,或手动输入密钥',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 二维码区域
|
||||
_buildQrCodeSection(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildStepCard(
|
||||
stepNumber: '3',
|
||||
title: '输入验证码完成绑定',
|
||||
description: '输入谷歌验证器显示的6位数字验证码',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 验证码输入
|
||||
_buildCodeInput(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 绑定按钮
|
||||
_buildSubmitButton(
|
||||
text: '绑定谷歌验证器',
|
||||
onTap: _bindGoogleAuth,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建已绑定状态内容
|
||||
Widget _buildBoundContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态提示
|
||||
_buildStatusCard(
|
||||
icon: Icons.verified_user,
|
||||
title: '已绑定',
|
||||
subtitle: '您的账户已启用谷歌验证器保护',
|
||||
color: const Color(0xFF4CAF50),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 提示卡片
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x33FFC107),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
color: Color(0xFFD4AF37),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
'安全提示',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'谷歌验证器已启用,每次登录和重要操作时需要输入验证码。如需解绑,请输入当前验证码。',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 验证码输入
|
||||
const Text(
|
||||
'输入验证码以解绑',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCodeInput(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 解绑按钮
|
||||
_buildSubmitButton(
|
||||
text: '解绑谷歌验证器',
|
||||
onTap: _unbindGoogleAuth,
|
||||
isDanger: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建状态卡片
|
||||
Widget _buildStatusCard({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建步骤卡片
|
||||
Widget _buildStepCard({
|
||||
required String stepNumber,
|
||||
required String title,
|
||||
required String description,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD4AF37),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
stepNumber,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建二维码区域
|
||||
Widget _buildQrCodeSection() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 二维码
|
||||
if (_otpAuthUri != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: _otpAuthUri!,
|
||||
version: QrVersions.auto,
|
||||
size: 180,
|
||||
backgroundColor: Colors.white,
|
||||
eyeStyle: const QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
dataModuleStyle: const QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 密钥显示
|
||||
const Text(
|
||||
'或手动输入密钥',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: _copySecret,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5E6),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0x33D4AF37),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_secret ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 2,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
Icons.copy,
|
||||
size: 20,
|
||||
color: Color(0xFFD4AF37),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'点击复制密钥',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
color: Color(0xFF8B5A2B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建验证码输入框
|
||||
Widget _buildCodeInput() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 54,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x80FFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0x80FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 8,
|
||||
height: 1.19,
|
||||
color: Color(0xFF5D4037),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 15),
|
||||
border: InputBorder.none,
|
||||
hintText: '000000',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 20,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 8,
|
||||
height: 1.19,
|
||||
color: Color(0x335D4037),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建提交按钮
|
||||
Widget _buildSubmitButton({
|
||||
required String text,
|
||||
required VoidCallback onTap,
|
||||
bool isDanger = false,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: _isSubmitting ? null : onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: isDanger
|
||||
? (_isSubmitting ? Colors.red.withValues(alpha: 0.5) : Colors.red)
|
||||
: (_isSubmitting ? const Color(0xFFD4AF37).withValues(alpha: 0.5) : const Color(0xFFD4AF37)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDanger
|
||||
? Colors.red.withValues(alpha: 0.3)
|
||||
: const Color(0x4DD4AF37),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.5,
|
||||
letterSpacing: 0.24,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@ import '../features/share/presentation/pages/share_page.dart';
|
|||
import '../features/deposit/presentation/pages/deposit_usdt_page.dart';
|
||||
import '../features/planting/presentation/pages/planting_quantity_page.dart';
|
||||
import '../features/planting/presentation/pages/planting_location_page.dart';
|
||||
import '../features/security/presentation/pages/google_auth_page.dart';
|
||||
import '../features/security/presentation/pages/change_password_page.dart';
|
||||
import '../features/security/presentation/pages/bind_email_page.dart';
|
||||
import 'route_paths.dart';
|
||||
import 'route_names.dart';
|
||||
|
||||
|
|
@ -211,6 +214,27 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
},
|
||||
),
|
||||
|
||||
// Google Auth Page (谷歌验证器)
|
||||
GoRoute(
|
||||
path: RoutePaths.googleAuth,
|
||||
name: RouteNames.googleAuth,
|
||||
builder: (context, state) => const GoogleAuthPage(),
|
||||
),
|
||||
|
||||
// Change Password Page (修改密码)
|
||||
GoRoute(
|
||||
path: RoutePaths.changePassword,
|
||||
name: RouteNames.changePassword,
|
||||
builder: (context, state) => const ChangePasswordPage(),
|
||||
),
|
||||
|
||||
// Bind Email Page (绑定邮箱)
|
||||
GoRoute(
|
||||
path: RoutePaths.bindEmail,
|
||||
name: RouteNames.bindEmail,
|
||||
builder: (context, state) => const BindEmailPage(),
|
||||
),
|
||||
|
||||
// Main Shell with Bottom Navigation
|
||||
ShellRoute(
|
||||
navigatorKey: _shellNavigatorKey,
|
||||
|
|
|
|||
Loading…
Reference in New Issue