Files
jizhi/frontend/lib/features/auth/pages/login_page.dart
T

209 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:miaoji_zhang/shared/api/api_client.dart';
import 'package:miaoji_zhang/shared/api/auth_api.dart';
import 'package:miaoji_zhang/shared/api/config_api.dart';
import 'package:miaoji_zhang/features/auth/local_data_recovery.dart';
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
import 'package:miaoji_zhang/shared/services/local_database.dart';
import 'package:miaoji_zhang/shared/services/session_store.dart';
import 'package:miaoji_zhang/shared/services/push_service.dart';
import 'package:miaoji_zhang/shared/theme/app_theme.dart';
import 'package:miaoji_zhang/shared/version.dart';
import 'package:miaoji_zhang/shared/widgets/app_controls.dart';
import 'package:miaoji_zhang/shared/widgets/brand_logo.dart';
class LoginPage extends StatefulWidget {
final String? notice;
const LoginPage({super.key, this.notice});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _user = TextEditingController();
final _pass = TextEditingController();
bool _loading = false;
@override
void initState() {
super.initState();
PublicConfigApi.init().ignore();
if (widget.notice case final notice? when notice.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(notice)));
}
});
}
}
@override
void dispose() {
_user.dispose();
_pass.dispose();
super.dispose();
}
Future<void> _login() async {
if (_user.text.trim().isEmpty || _pass.text.isEmpty) {
_showMessage('请输入用户名和密码');
return;
}
final guestSnapshot = SessionStore.instance.isGuest
? LocalDatabase.instance.guestSnapshot()
: null;
setState(() => _loading = true);
try {
final result = await AuthApi.login(_user.text.trim(), _pass.text);
final profile = result.profile;
await PushService.instance.refresh();
await CurrentLedgerStore.instance.ensureLoaded(force: true);
if (!mounted) return;
await offerLocalDataRecovery(context, guestSnapshot: guestSnapshot);
if (!mounted) return;
if (result.accountClosureCancelled) _showMessage('已取消账号注销,欢迎回来');
final destination = !profile.onboardingDone
? '/onboarding'
: profile.appMode == 'ai'
? '/ai-mode'
: '/home';
context.go(destination);
} catch (error) {
if (mounted) _showMessage(apiErrorMessage(error));
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _startGuest() async {
setState(() => _loading = true);
try {
await SessionStore.instance.startGuest();
await CurrentLedgerStore.instance.ensureLoaded(force: true);
if (mounted) context.go('/home');
} catch (error) {
if (mounted) _showMessage(apiErrorMessage(error));
} finally {
if (mounted) setState(() => _loading = false);
}
}
void _showMessage(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 28),
children: [
SizedBox(height: 48),
Center(
child: Column(
children: [
const BrandLogo(size: 82),
SizedBox(height: 16),
Text(
PublicConfigApi.appName,
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w800,
letterSpacing: 1,
),
),
SizedBox(height: 8),
Text(
PublicConfigApi.slogan,
style: TextStyle(fontSize: 13, color: context.jz.text2),
),
SizedBox(height: 48),
],
),
),
TextField(
controller: _user,
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: '用户名', hintText: '请输入用户名'),
),
SizedBox(height: 12),
TextField(
controller: _pass,
obscureText: true,
decoration: InputDecoration(labelText: '密码', hintText: '请输入密码'),
onSubmitted: (_) => _loading ? null : _login(),
),
SizedBox(height: 22),
JzActionButton(
label: '登录',
loading: _loading,
onPressed: _loading ? null : _login,
),
SizedBox(height: 10),
JzActionButton(
label: '游客模式',
secondary: true,
onPressed: _loading ? null : _startGuest,
),
SizedBox(height: 7),
Text(
'游客数据仅保存在本机;AI、语音解析和图片识别需登录联网',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10.5,
color: context.jz.text3,
height: 1.45,
),
),
SizedBox(height: 8),
Center(
child: TextButton(
onPressed: _loading ? null : () => context.go('/register'),
child: Text('没有账号?去注册'),
),
),
const LoginLegalLinks(),
SizedBox(height: 20),
Center(
child: Text(
AppVersion.display,
style: TextStyle(fontSize: 10, color: context.jz.text3),
),
),
],
),
),
);
}
}
class LoginLegalLinks extends StatelessWidget {
const LoginLegalLinks({super.key});
@override
Widget build(BuildContext context) {
return Wrap(
alignment: WrapAlignment.center,
spacing: 2,
runSpacing: 0,
children: [
_link(context, '用户协议', 'terms'),
_link(context, '隐私政策', 'privacy'),
],
);
}
Widget _link(BuildContext context, String label, String kind) => TextButton(
onPressed: () => context.push('/legal/$kind'),
child: Text(label, style: TextStyle(fontSize: 11.5)),
);
}