import 'package:flutter/cupertino.dart'; import '../api/api_client.dart'; /// 独立登录页,对应 Android 端 `activity/login/LoginActivity`。 /// /// 行为与 Android 端保持一致: /// - 用户名 / 密码双输入,均非空才允许提交; /// - 支持注册(`POST /api/auth/register`),注册成功后停留在登录页提示登录; /// - 登录成功(`POST /api/auth/login`)后持久化令牌并回显用户名, /// 通过命名路由 `/home` 替换栈顶进入主页; /// - 提交过程中禁用输入与按钮,展示加载指示。 class LoginPage extends StatefulWidget { const LoginPage({super.key, required this.apiClient, this.message}); final ApiClient apiClient; /// 从主页因登录失效跳转过来时展示的提示文案。 final String? message; @override State createState() => _LoginPageState(); } class _LoginPageState extends State { final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _passwordFocus = FocusNode(); bool _busy = false; String? _error; @override void initState() { super.initState(); _error = widget.message; // 回显上次登录的用户名(对应 LoginViewModel.lastUsername())。 widget.apiClient.getUsername().then((name) { if (mounted && name != null && name.isNotEmpty) { _usernameController.text = name; } }); } @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _passwordFocus.dispose(); super.dispose(); } /// 读取并校验输入;返回 null 表示校验未通过(错误已展示)。 ({String username, String password})? _readInput() { final username = _usernameController.text.trim(); final password = _passwordController.text.trim(); if (username.isEmpty || password.isEmpty) { setState(() => _error = '请输入用户名和密码'); return null; } return (username: username, password: password); } Future _login() async { final input = _readInput(); if (input == null) return; setState(() { _busy = true; _error = null; }); try { await widget.apiClient.login(input.username, input.password); if (!mounted) return; Navigator.of(context).pushReplacementNamed('/home'); } catch (e) { if (!mounted) return; setState(() { _busy = false; _error = _describe(e, '登录失败'); }); } } Future _register() async { final input = _readInput(); if (input == null) return; setState(() { _busy = true; _error = null; }); try { await widget.apiClient.register(input.username, input.password); if (!mounted) return; setState(() { _busy = false; _error = '注册成功,请登录'; }); } catch (e) { if (!mounted) return; setState(() { _busy = false; _error = _describe(e, '注册失败'); }); } } /// 优先展示服务端返回的错误文案,其余异常回落到通用提示。 String _describe(Object e, String fallback) { if (e is ApiException) return e.code; return '$fallback:$e'; } @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(middle: Text('登录')), child: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Icon( CupertinoIcons.device_phone_portrait, size: 56, color: CupertinoColors.activeBlue, ), const SizedBox(height: 16), const Text( 'WebRTC 控制端', textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( '登录后可连接已绑定的被控设备', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: CupertinoColors.systemGrey, ), ), const SizedBox(height: 32), CupertinoTextField( controller: _usernameController, enabled: !_busy, placeholder: '用户名', prefix: const Padding( padding: EdgeInsets.only(left: 12), child: Icon(CupertinoIcons.person, size: 20, color: CupertinoColors.systemGrey), ), textInputAction: TextInputAction.next, autocorrect: false, onSubmitted: (_) => _passwordFocus.requestFocus(), padding: const EdgeInsets.symmetric( vertical: 14, horizontal: 12), ), const SizedBox(height: 12), CupertinoTextField( controller: _passwordController, focusNode: _passwordFocus, enabled: !_busy, placeholder: '密码', obscureText: true, prefix: const Padding( padding: EdgeInsets.only(left: 12), child: Icon(CupertinoIcons.lock, size: 20, color: CupertinoColors.systemGrey), ), textInputAction: TextInputAction.done, onSubmitted: (_) => _busy ? null : _login(), padding: const EdgeInsets.symmetric( vertical: 14, horizontal: 12), ), if (_error != null) ...[ const SizedBox(height: 12), Text( _error!, textAlign: TextAlign.center, style: const TextStyle( fontSize: 13, color: CupertinoColors.destructiveRed, ), ), ], const SizedBox(height: 24), CupertinoButton.filled( onPressed: _busy ? null : _login, child: _busy ? const CupertinoActivityIndicator( color: CupertinoColors.white) : const Text('登录'), ), const SizedBox(height: 8), CupertinoButton( onPressed: _busy ? null : _register, child: const Text('注册新账号'), ), ], ), ), ), ), ), ); } }