refactor: 移除前端 Mock API,新增共享组件,优化认证流程
- 删除 mock_api_client、mock_calendar_service、mock_history_service - 新增 fixed_length_code_input、link_button、message_composer 共享组件 - 优化登录/注册/密码重置页面使用新组件 - 简化 injection.dart 移除 mock 分支 - 更新 env.dart 配置(BACKEND_URL 替换 API_URL) - 后端 agentscope 工具和测试更新 - 重构 AGENTS.md 文档结构 - 新增 deploy/ 目录和 protocol 文档
This commit is contained in:
@@ -6,6 +6,7 @@ import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/banner/app_banner.dart';
|
||||
import '../../../../shared/widgets/link_button.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../widgets/auth_page_scaffold.dart';
|
||||
import '../../presentation/cubits/login_cubit.dart';
|
||||
@@ -225,30 +226,16 @@ class _LoginViewState extends State<LoginView> {
|
||||
}
|
||||
|
||||
Widget _buildForgotPassword() {
|
||||
return GestureDetector(
|
||||
return LinkButton(
|
||||
text: '忘记密码?',
|
||||
onTap: () => context.push('/reset-password'),
|
||||
child: const Text(
|
||||
'忘记密码?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
return GestureDetector(
|
||||
return LinkButton(
|
||||
text: '还没有账号?去注册',
|
||||
onTap: () => context.push('/register'),
|
||||
child: const Text(
|
||||
'还没有账号?去注册',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/banner/app_banner.dart';
|
||||
import '../../../../shared/widgets/fixed_length_code_input.dart';
|
||||
import '../../../../shared/widgets/link_button.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../presentation/cubits/register_cubit.dart';
|
||||
@@ -34,6 +36,41 @@ class RegisterView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RegisterViewState extends State<RegisterView> {
|
||||
static const _inviteCodeLength = 4;
|
||||
static const _inviteAllowedChars = <String>{
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'J',
|
||||
'K',
|
||||
'M',
|
||||
'N',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
};
|
||||
|
||||
final _nicknameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
@@ -51,10 +88,15 @@ class _RegisterViewState extends State<RegisterView> {
|
||||
|
||||
Future<void> _handleNext() async {
|
||||
final cubit = context.read<RegisterCubit>();
|
||||
final inviteCode = _inviteCodeController.text.trim().toUpperCase();
|
||||
final normalizedInviteCode = inviteCode.length == _inviteCodeLength
|
||||
? inviteCode
|
||||
: '';
|
||||
|
||||
cubit.usernameChanged(_nicknameController.text);
|
||||
cubit.emailChanged(_emailController.text);
|
||||
cubit.passwordChanged(_passwordController.text);
|
||||
cubit.inviteCodeChanged(_inviteCodeController.text);
|
||||
cubit.inviteCodeChanged(normalizedInviteCode);
|
||||
|
||||
if (!cubit.state.isStep1Valid || cubit.state.isSending) {
|
||||
String? errorMsg;
|
||||
@@ -71,6 +113,14 @@ class _RegisterViewState extends State<RegisterView> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inviteCode.isNotEmpty && normalizedInviteCode.isEmpty && mounted) {
|
||||
Toast.show(
|
||||
context,
|
||||
'邀请码需为 4 位,且仅支持 A-H/J-N/P-Z 与 2-9;已按无邀请码继续注册',
|
||||
type: ToastType.warning,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
context.push('/register/verification', extra: cubit);
|
||||
}
|
||||
@@ -147,7 +197,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||
const SizedBox(height: 12),
|
||||
_buildPasswordInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildInput('邀请码(选填)', '请输入邀请码', _inviteCodeController),
|
||||
_buildInviteCodeInput(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStepIndicator(),
|
||||
if (state.errorMessage != null)
|
||||
@@ -174,6 +224,42 @@ class _RegisterViewState extends State<RegisterView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInviteCodeInput() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'邀请码(选填)',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
FixedLengthCodeInput(
|
||||
controller: _inviteCodeController,
|
||||
length: _inviteCodeLength,
|
||||
semanticLabel: '邀请码输入框',
|
||||
uppercase: true,
|
||||
allowedCharacters: _inviteAllowedChars,
|
||||
onChanged: (value) {
|
||||
context.read<RegisterCubit>().inviteCodeChanged(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'4 位邀请码,支持 A-H/J-N/P-Z 与 2-9',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInput(
|
||||
String label,
|
||||
String hint,
|
||||
@@ -262,16 +348,6 @@ class _RegisterViewState extends State<RegisterView> {
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
return GestureDetector(
|
||||
onTap: () => context.pop(),
|
||||
child: const Text(
|
||||
'已有账号?去登录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
);
|
||||
return LinkButton(text: '已有账号?去登录', onTap: () => context.pop());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:formz/formz.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/fixed_length_code_input.dart';
|
||||
import '../../../../shared/widgets/link_button.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../presentation/cubits/register_cubit.dart';
|
||||
@@ -49,22 +51,10 @@ class _RegisterVerificationViewState extends State<RegisterVerificationView> {
|
||||
Timer? _countdownTimer;
|
||||
int _countdown = 0;
|
||||
bool _firstSendCompleted = false;
|
||||
bool _hintShown = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_hintShown) {
|
||||
_hintShown = true;
|
||||
Toast.show(
|
||||
context,
|
||||
'验证码已发送,如未收到请检查垃圾邮件或确认邮箱已注册',
|
||||
type: ToastType.info,
|
||||
duration: const Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -200,6 +190,12 @@ class _RegisterVerificationViewState extends State<RegisterVerificationView> {
|
||||
!_firstSendCompleted) {
|
||||
_firstSendCompleted = true;
|
||||
_startCountdown();
|
||||
Toast.show(
|
||||
context,
|
||||
'验证码已发送,如未收到请检查垃圾邮件或确认邮箱已注册',
|
||||
type: ToastType.info,
|
||||
duration: const Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
@@ -246,16 +242,28 @@ class _RegisterVerificationViewState extends State<RegisterVerificationView> {
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
child: FixedLengthCodeInput(
|
||||
controller: _codeController,
|
||||
length: 6,
|
||||
semanticLabel: '邮箱验证码输入框',
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '输入验证码',
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
allowedCharacters: const {
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
},
|
||||
onChanged: (value) {
|
||||
context.read<RegisterCubit>().verificationCodeChanged(
|
||||
value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -268,36 +276,38 @@ class _RegisterVerificationViewState extends State<RegisterVerificationView> {
|
||||
}
|
||||
|
||||
Widget _buildResendButton(bool canResend, RegisterState state) {
|
||||
final bgColor = canResend ? AppColors.primary : const Color(0xFFF1F5F9);
|
||||
final textColor = canResend ? AppColors.white : AppColors.slate400;
|
||||
final canPress =
|
||||
canResend && state.status != FormzSubmissionStatus.inProgress;
|
||||
|
||||
String text;
|
||||
if (state.status == FormzSubmissionStatus.inProgress) {
|
||||
text = '发送中';
|
||||
} else if (canResend) {
|
||||
text = '重发';
|
||||
} else {
|
||||
text = '${_countdown}s';
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: canResend ? _handleResendCode : null,
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textColor,
|
||||
return SizedBox(
|
||||
width: 70,
|
||||
height: 44,
|
||||
child: TextButton(
|
||||
onPressed: canPress ? _handleResendCode : null,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: canResend ? AppColors.primary : AppColors.slate100,
|
||||
foregroundColor: canResend ? AppColors.white : AppColors.slate400,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
child: state.status == FormzSubmissionStatus.inProgress
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.slate400),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
canResend ? '重发' : '${_countdown}s',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -329,16 +339,6 @@ class _RegisterVerificationViewState extends State<RegisterVerificationView> {
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
return GestureDetector(
|
||||
onTap: () => context.go('/'),
|
||||
child: const Text(
|
||||
'已有账号?去登录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
),
|
||||
);
|
||||
return LinkButton(text: '已有账号?去登录', onTap: () => context.go('/'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/fixed_length_code_input.dart';
|
||||
import '../../../../shared/widgets/link_button.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../presentation/cubits/reset_password_cubit.dart';
|
||||
@@ -114,7 +116,7 @@ class _ResetPasswordViewState extends State<ResetPasswordView> {
|
||||
children: [
|
||||
_buildEmailInput(state.email.displayError != null),
|
||||
const SizedBox(height: 12),
|
||||
_buildCodeInput(state.code.displayError != null, state),
|
||||
_buildCodeInput(state),
|
||||
const SizedBox(height: 12),
|
||||
_buildPasswordInput(state.newPassword.displayError != null),
|
||||
const SizedBox(height: 12),
|
||||
@@ -160,7 +162,7 @@ class _ResetPasswordViewState extends State<ResetPasswordView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeInput(bool hasError, ResetPasswordState state) {
|
||||
Widget _buildCodeInput(ResetPasswordState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -176,16 +178,26 @@ class _ResetPasswordViewState extends State<ResetPasswordView> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
child: FixedLengthCodeInput(
|
||||
controller: _codeController,
|
||||
length: 6,
|
||||
semanticLabel: '重置密码验证码输入框',
|
||||
keyboardType: TextInputType.number,
|
||||
allowedCharacters: const {
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
},
|
||||
onChanged: (value) {
|
||||
context.read<ResetPasswordCubit>().codeChanged(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入 6 位验证码',
|
||||
errorText: hasError ? ' ' : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -325,17 +337,6 @@ class _ResetPasswordViewState extends State<ResetPasswordView> {
|
||||
}
|
||||
|
||||
Widget _buildBackToLogin() {
|
||||
return GestureDetector(
|
||||
onTap: () => context.go('/'),
|
||||
child: const Text(
|
||||
'返回登录',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
return LinkButton(text: '返回登录', onTap: () => context.go('/'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user