Files
photo-live/apps/mobile/lib/pages/login_page.dart
T
2026-09-21 17:20:43 +08:00

145 lines
5.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 登录页(UI-01 §4.5 主题统一,P3 计划 Task 3 基础)。
///
/// 账号/密码表单 → [ApiClient.login](会话由客户端自动持久化到
/// SessionStore)→ 成功 pushReplacement /home;失败以内联 StateBadge
/// (图标 + 文案 + 错误色)展示错误码本地化文案(AUTH_RATE_LIMITED 文案
/// 自带「稍后再试」;网络/校验错误各有专属文案)。提交期间按钮进入
/// 加载态防重复提交。
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api_client.dart';
import '../core/app_exception.dart';
import '../core/error_l10n.dart';
import '../features/uploads/engine_scope.dart';
import '../l10n/app_localizations.dart';
import '../ui/components/state_badge.dart';
import '../ui/tokens.dart';
/// 登录页。
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@override
ConsumerState<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _accountController = TextEditingController();
final _passwordController = TextEditingController();
bool _submitting = false;
/// 最近一次提交失败文案(内联 StateBadge 展示;null = 无错误)。
String? _errorText;
@override
void dispose() {
_accountController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_submitting = true;
_errorText = null;
});
final l10n = AppLocalizations.of(context);
final navigator = Navigator.of(context);
try {
await ref.read(apiClientProvider).login(
_accountController.text.trim(),
_passwordController.text,
);
// 登录成功:刷新会话账号 → 引擎以新账号 scope start(Task 9 接线)。
await ref.read(sessionAccountProvider.notifier).refresh();
navigator.pushReplacementNamed('/home');
} on AppException catch (error) {
// 429(AUTH_RATE_LIMITED)等文案经错误码映射;登录页的
// AUTH_INVALID_CREDENTIALS 即「账号或密码错误」。
if (mounted) setState(() => _errorText = l10n.messageForError(error));
} on Object {
if (mounted) setState(() => _errorText = l10n.errorGeneric);
} finally {
if (mounted) setState(() => _submitting = false);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appName)),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Padding(
padding: const EdgeInsets.all(kSpace24),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _accountController,
autofillHints: const [AutofillHints.username],
decoration: InputDecoration(labelText: l10n.username),
validator: (value) =>
(value == null || value.trim().isEmpty)
? l10n.fieldRequired
: null,
),
const SizedBox(height: kSpace16),
TextFormField(
controller: _passwordController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
autofillHints: const [AutofillHints.password],
onFieldSubmitted: (_) => _submitting ? null : _submit(),
decoration: InputDecoration(labelText: l10n.password),
validator: (value) => (value == null || value.isEmpty)
? l10n.fieldRequired
: null,
),
if (_errorText != null) ...[
const SizedBox(height: kSpace16),
StateBadge(
variant: BadgeVariant.error,
label: _errorText!,
),
],
const SizedBox(height: kSpace24),
FilledButton(
onPressed: _submitting ? null : _submit,
child: _submitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(l10n.login),
),
const SizedBox(height: kSpace12),
Text(
l10n.loginHint,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
),
),
);
}
}