// 班级码加入页面 — 学生/家长通过 6 位码加入班级 // // 设计要点: // - 6 位独立输入框,自动聚焦下一位 // - 输入完成后自动提交验证 // - 安全限制:5 次错误后锁定 30 分钟 // - 友好的状态反馈(验证中/成功/失败) import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../core/constants/design_tokens.dart'; import '../bloc/auth_bloc.dart'; /// 班级码加入页面 class ClassCodeJoinPage extends StatefulWidget { const ClassCodeJoinPage({super.key}); @override State createState() => _ClassCodeJoinPageState(); } class _ClassCodeJoinPageState extends State { final List _controllers = List.generate( DesignTokens.classCodeLength, (_) => TextEditingController(), ); final List _focusNodes = List.generate( DesignTokens.classCodeLength, (_) => FocusNode(), ); @override void initState() { super.initState(); // 自动聚焦第一个输入框 WidgetsBinding.instance.addPostFrameCallback((_) { _focusNodes[0].requestFocus(); }); } @override void dispose() { for (final c in _controllers) { c.dispose(); } for (final f in _focusNodes) { f.dispose(); } super.dispose(); } /// 获取当前输入的班级码 String get _classCode => _controllers.map((c) => c.text).join(); /// 是否所有位都已输入 bool get _isComplete => _controllers.every((c) => c.text.isNotEmpty); void _onChanged(int index, String value) { if (value.isEmpty && index > 0) { // 退格清空 → 跳到前一位 _focusNodes[index - 1].requestFocus(); } else if (value.isNotEmpty && index < DesignTokens.classCodeLength - 1) { // 输入字符 → 跳到下一位 _focusNodes[index + 1].requestFocus(); } // 全部输入完成 → 自动提交 if (_isComplete) { _submit(); } } void _submit() { if (!_isComplete) return; context.read().add(ClassCodeSubmitted(_classCode)); // 提交后跳转到首页(班级码验证由 BLoC 处理) final state = context.read().state; if (state is Authenticated && !state.needsClassCode) { context.go('/home'); } } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.spacing24, ), child: Column( children: [ const Spacer(flex: 2), // 标题 Icon( Icons.groups_rounded, size: 64, color: colorScheme.primary, ), const SizedBox(height: DesignTokens.spacing24), Text( '加入班级', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: DesignTokens.spacing8), Text( '输入老师提供的 6 位班级码', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), ), ), const SizedBox(height: DesignTokens.spacing48), // 6 位输入框 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate( DesignTokens.classCodeLength, (index) => _buildCodeInput(context, index, colorScheme), ), ), const SizedBox(height: DesignTokens.spacing24), // 跳过按钮 TextButton( onPressed: () => context.go('/home'), child: Text( '稍后再加入', style: TextStyle( color: colorScheme.onSurface.withValues(alpha: 0.5), ), ), ), const Spacer(flex: 3), ], ), ), ), ); } /// 单个班级码输入框 Widget _buildCodeInput(BuildContext context, int index, ColorScheme colorScheme) { return SizedBox( width: 48, height: 56, child: TextField( controller: _controllers[index], focusNode: _focusNodes[index], textAlign: TextAlign.center, textAlignVertical: TextAlignVertical.center, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, letterSpacing: 0, ), maxLength: 1, keyboardType: TextInputType.text, textCapitalization: TextCapitalization.characters, decoration: InputDecoration( counterText: '', filled: true, fillColor: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: colorScheme.primary, width: 2), ), ), onChanged: (value) => _onChanged(index, value), onSubmitted: (_) { if (_isComplete) _submit(); }, ), ); } }