fix(app): account-switch data residue + empty-albums pull-to-refresh

- 换账号数据残留(用户报告):albumListProvider 非 autoDispose 且首页
  IndexedStack 常驻——登出与登录成功两处显式 invalidate(任务库本就按账号
  隔离键隔离,不受影响)
- 空相册无法下拉刷新(用户报告):空态原本直接 return EmptyState 无
  RefreshIndicator——被拉进相册后无法手动刷新;空态改入可滚动容器承载
  下拉刷新(保留新建入口)
- 397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 12:05:25 +08:00
parent c1869d7314
commit 20321f1b93
3 changed files with 85 additions and 60 deletions
+49 -43
View File
@@ -13,6 +13,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/api_client.dart';
import '../features/albums/providers.dart';
import '../core/app_exception.dart';
import '../core/error_l10n.dart';
import '../features/uploads/engine_scope.dart';
@@ -56,12 +57,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
final l10n = AppLocalizations.of(context);
final navigator = Navigator.of(context);
try {
await ref.read(apiClientProvider).login(
_accountController.text.trim(),
_passwordController.text,
);
await ref
.read(apiClientProvider)
.login(_accountController.text.trim(), _passwordController.text);
// 登录成功:刷新会话账号 → 引擎以新账号 scope start(Task 9 接线)。
await ref.read(sessionAccountProvider.notifier).refresh();
// 账号级缓存失效(换账号登录/会话过期重登同一保障:列表等 Provider
// 非 autoDispose,首页 IndexedStack 常驻——不失效会短暂显示上一账号数据)。
ref.invalidate(albumListProvider);
navigator.pushReplacementNamed('/home');
} on AppException catch (error) {
// 429(AUTH_RATE_LIMITED)等文案经错误码映射;登录页的
@@ -84,17 +87,16 @@ class _LoginPageState extends ConsumerState<LoginPage> {
top: false,
child: LayoutBuilder(
builder: (context, constraints) {
final keyboardOpen =
MediaQuery.of(context).viewInsets.bottom > 0;
final keyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0;
// 正常高度 28%(25%-32% 区间);键盘/短屏压缩至 10%。
final headerHeight = (constraints.maxHeight *
(keyboardOpen ? 0.10 : 0.28))
.clamp(96.0, 320.0);
final headerHeight =
(constraints.maxHeight * (keyboardOpen ? 0.10 : 0.28)).clamp(
96.0,
320.0,
);
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
@@ -102,18 +104,18 @@ class _LoginPageState extends ConsumerState<LoginPage> {
_CollageHeader(height: headerHeight),
Padding(
padding: const EdgeInsets.fromLTRB(
kSpace24, kSpace24, kSpace24, kSpace24,
kSpace24,
kSpace24,
kSpace24,
kSpace24,
),
child: ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: 460),
constraints: const BoxConstraints(maxWidth: 460),
child: Form(
key: _formKey,
autovalidateMode:
AutovalidateMode.onUserInteraction,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_BrandRow(),
const SizedBox(height: kSpace16),
@@ -136,15 +138,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
const SizedBox(height: kSpace24),
TextFormField(
controller: _accountController,
autofillHints: const [
AutofillHints.username,
],
autofillHints: const [AutofillHints.username],
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: l10n.username,
),
validator: (value) => (value == null ||
value.trim().isEmpty)
validator: (value) =>
(value == null || value.trim().isEmpty)
? l10n.fieldRequired
: null,
),
@@ -154,9 +154,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
obscureText: _obscurePassword,
enableSuggestions: false,
autocorrect: false,
autofillHints: const [
AutofillHints.password,
],
autofillHints: const [AutofillHints.password],
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) =>
_submitting ? null : _submit(),
@@ -174,15 +172,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons
.visibility_off_outlined,
: Icons.visibility_off_outlined,
size: 24,
),
),
),
),
validator: (value) => (value == null ||
value.isEmpty)
validator: (value) =>
(value == null || value.isEmpty)
? l10n.fieldRequired
: null,
),
@@ -211,8 +208,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
l10n.loginAskAdmin,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color:
theme.colorScheme.onSurfaceVariant,
color: theme.colorScheme.onSurfaceVariant,
),
),
],
@@ -267,20 +263,32 @@ class _CollageHeader extends StatelessWidget {
Positioned(
left: 28,
top: 24,
child: _tile(120, 84, Colors.white.withValues(alpha: 0.16),
rotate: -0.08),
child: _tile(
120,
84,
Colors.white.withValues(alpha: 0.16),
rotate: -0.08,
),
),
Positioned(
right: 44,
top: 40,
child: _tile(132, 92, Colors.white.withValues(alpha: 0.12),
rotate: 0.06),
child: _tile(
132,
92,
Colors.white.withValues(alpha: 0.12),
rotate: 0.06,
),
),
Positioned(
left: 96,
bottom: 18,
child: _tile(150, 72, Colors.white.withValues(alpha: 0.10),
rotate: -0.04),
child: _tile(
150,
72,
Colors.white.withValues(alpha: 0.10),
rotate: -0.04,
),
),
// 右上语言选择(浮于拼贴之上)。
Positioned(
@@ -293,8 +301,7 @@ class _CollageHeader extends StatelessWidget {
);
}
Widget _tile(double width, double height, Color color,
{double rotate = 0}) {
Widget _tile(double width, double height, Color color, {double rotate = 0}) {
return Transform.rotate(
angle: rotate,
child: Container(
@@ -349,8 +356,7 @@ class _LanguagePill extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context);
final isZh =
Localizations.maybeLocaleOf(context)?.languageCode == 'zh';
final isZh = Localizations.maybeLocaleOf(context)?.languageCode == 'zh';
return Semantics(
label: l10n.language,
button: true,