import 'package:flutter/material.dart'; import '../api/oa_client.dart'; import '../app_config.dart'; import '../pages/legal_page.dart'; import '../session/session.dart'; import '../theme.dart'; import '../widgets/beian_footer.dart'; import '../widgets/wecom.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key, required this.session, required this.api}); final SessionStore session; final OaClient api; @override State createState() => _LoginPageState(); } class _LoginPageState extends State { final _phone = TextEditingController(); final _pass = TextEditingController(); final _sms = TextEditingController(); bool _loading = false; bool _smsLogin = false; bool _agreed = false; String _error = ''; int _wait = 0; @override void initState() { super.initState(); _loadOptions(); } Future _loadOptions() async { try { final data = await widget.api.get('/auth/login-options'); if (data is Map && mounted) setState(() => _smsLogin = data['smsLogin'] == true); } catch (_) {} } Future _sendSms() async { if (_phone.text.trim().isEmpty) { setState(() => _error = '请先填写手机号'); return; } setState(() => _error = ''); try { await widget.api.post('/auth/sms-code', {'username': _phone.text.trim()}); setState(() => _wait = 60); _tick(); } catch (e) { setState(() => _error = '$e'); } } void _tick() { Future.delayed(const Duration(seconds: 1), () { if (!mounted || _wait <= 0) return; setState(() => _wait--); if (_wait > 0) _tick(); }); } Future _openLegal(String kind) async { await Navigator.of(context).push( MaterialPageRoute(builder: (_) => LegalPage(api: widget.api, kind: kind)), ); } Future _submit() async { if (!_agreed) { setState(() => _error = '请先阅读并同意用户协议和隐私政策'); return; } setState(() { _loading = true; _error = ''; }); try { final data = await widget.api.post('/auth/login', { 'username': _phone.text.trim(), 'password': _pass.text, 'agreeTerms': true, 'clientKind': 'mobile', if (_smsLogin && _sms.text.trim().isNotEmpty) 'smsCode': _sms.text.trim(), }); await widget.session.applyLogin(Map.from(data as Map)); try { final menus = await widget.api.get('/system/menus'); widget.session.setMenus(asMaps(menus).map(MenuNode.fromJson).toList()); } catch (_) {} } catch (e) { if (mounted) setState(() => _error = '$e'); } finally { if (mounted) setState(() => _loading = false); } } @override void dispose() { _phone.dispose(); _pass.dispose(); _sms.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.fromLTRB(28, 48, 28, 16), children: [ const SquareAvatar(label: '风', size: 64, color: kBind, icon: Icons.apartment), const SizedBox(height: 16), const Text(AppConfig.shortName, textAlign: TextAlign.center, style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700)), const SizedBox(height: 4), const Text('手机号登录', textAlign: TextAlign.center, style: TextStyle(color: kMute)), const SizedBox(height: 36), if (_error.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 12), child: Text(_error, style: const TextStyle(color: kDanger)), ), TextField( controller: _phone, keyboardType: TextInputType.phone, decoration: const InputDecoration(hintText: '手机号'), ), const SizedBox(height: 12), TextField( controller: _pass, obscureText: true, decoration: const InputDecoration(hintText: '密码'), ), if (_smsLogin) ...[ const SizedBox(height: 12), Row( children: [ Expanded(child: TextField(controller: _sms, decoration: const InputDecoration(hintText: '短信验证码'))), const SizedBox(width: 8), TextButton(onPressed: _wait > 0 ? null : _sendSms, child: Text(_wait > 0 ? '${_wait}s' : '获取验证码')), ], ), ], const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 24, height: 24, child: Checkbox( value: _agreed, onChanged: (v) => setState(() => _agreed = v == true), ), ), const SizedBox(width: 4), Expanded( child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ const Text('我已阅读并同意', style: TextStyle(fontSize: 13, color: kMute)), GestureDetector( onTap: () => _openLegal('user-agreement'), child: const Text('《用户协议》', style: TextStyle(fontSize: 13, color: kBind)), ), const Text('和', style: TextStyle(fontSize: 13, color: kMute)), GestureDetector( onTap: () => _openLegal('privacy'), child: const Text('《隐私政策》', style: TextStyle(fontSize: 13, color: kBind)), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 46, child: FilledButton( onPressed: _loading || !_agreed ? null : _submit, child: Text(_loading ? '登录中…' : '登录', style: const TextStyle(fontSize: 16)), ), ), ], ), ), const BeianFooter(), ], ), ), ), ), ); } }