Initial project import
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
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/shared/services/current_ledger_store.dart';
|
||||
import 'package:miaoji_zhang/shared/services/guest_merge_service.dart';
|
||||
import 'package:miaoji_zhang/shared/services/local_database.dart';
|
||||
import 'package:miaoji_zhang/shared/services/session_store.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 closureCancelled = await AuthApi.login(
|
||||
_user.text.trim(),
|
||||
_pass.text,
|
||||
);
|
||||
final profile = await AuthApi.me();
|
||||
await CurrentLedgerStore.instance.ensureLoaded(force: true);
|
||||
if (!mounted) return;
|
||||
if (guestSnapshot?['hasData'] == true) {
|
||||
final shouldMerge = await showJzConfirmSheet(
|
||||
context,
|
||||
title: '发现游客数据',
|
||||
message: '是否将本机游客账单、分类和预算导入当前账号?数据会放入独立的“游客数据”账本,原游客数据仍保留在本机。',
|
||||
confirmLabel: '导入数据',
|
||||
);
|
||||
if (shouldMerge) {
|
||||
try {
|
||||
final result = await GuestMergeService.merge(guestSnapshot!);
|
||||
if (mounted) {
|
||||
_showMessage('已导入 ${result.transactionCount} 笔游客账单');
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
_showMessage('游客数据暂未导入:${apiErrorMessage(error)}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
if (closureCancelled) _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)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user