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
+20 -6
View File
@@ -101,11 +101,23 @@ class AlbumListBody extends ConsumerWidget {
onRetry: notifier.refresh,
);
}
return EmptyState(
icon: Icons.photo_library_outlined,
title: l10n.albumsEmpty,
actionLabel: l10n.albumCreate,
onAction: () => AlbumsTab.openCreate(context, ref),
// 空态也保留下拉刷新(被拉进相册后可手动刷新看到,2026-09-23 用户报告)。
return RefreshIndicator(
onRefresh: notifier.refresh,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: EmptyState(
icon: Icons.photo_library_outlined,
title: l10n.albumsEmpty,
actionLabel: l10n.albumCreate,
onAction: () => AlbumsTab.openCreate(context, ref),
),
),
],
),
);
}
@@ -147,7 +159,9 @@ class AlbumListBody extends ConsumerWidget {
);
}
// 接近底部时预取下一页(loadMore 自带并发/末页守卫)。
if (index >= rowCount - 2 && state.hasMore && !state.loadingMore) {
if (index >= rowCount - 2 &&
state.hasMore &&
!state.loadingMore) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => notifier.loadMore(),
);
+16 -11
View File
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/locale_controller.dart';
import '../core/api_client.dart';
import '../features/albums/providers.dart';
import '../features/uploads/engine_scope.dart';
import '../features/uploads/queue_controller.dart';
import '../l10n/app_localizations.dart';
@@ -136,9 +137,7 @@ class SettingsTab extends ConsumerWidget {
child: SingleChildScrollView(
// 长文案/大字体下允许滚动,面板不做固定高度。
child: Padding(
padding: const EdgeInsets.fromLTRB(
kSpace24, 0, kSpace24, kSpace24,
),
padding: const EdgeInsets.fromLTRB(kSpace24, 0, kSpace24, kSpace24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -206,6 +205,10 @@ class SettingsTab extends ConsumerWidget {
await ref.read(apiClientProvider).logout();
// 会话账号失效 → 引擎 stop(任务保留库内,见 engine_scope.dart)。
await ref.read(sessionAccountProvider.notifier).refresh();
// 账号级缓存失效(2026-09-23 用户报告:换账号后相册列表仍是上一账号数据
// ——首页 IndexedStack 三 tab 常驻不销毁,非 autoDispose 的列表 Provider
// 必须显式失效;任务库已按账号隔离键隔离,无需清)。
ref.invalidate(albumListProvider);
if (!context.mounted) return;
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
}
@@ -226,7 +229,10 @@ class SettingsTab extends ConsumerWidget {
// ── 账号摘要卡(只读展示,非编辑入口) ─────────────────
Padding(
padding: const EdgeInsets.fromLTRB(
kSpace16, kSpace8, kSpace16, kSpace8,
kSpace16,
kSpace8,
kSpace16,
kSpace8,
),
child: SectionHeader(title: l10n.settingsSectionAccount),
),
@@ -263,7 +269,10 @@ class SettingsTab extends ConsumerWidget {
// ── 通用:语言 / 改密 / 上传帮助 / 版本 ──────────────────
Padding(
padding: const EdgeInsets.fromLTRB(
kSpace16, kSpace24, kSpace16, kSpace8,
kSpace16,
kSpace24,
kSpace16,
kSpace8,
),
child: SectionHeader(title: l10n.settingsSectionGeneral),
),
@@ -323,8 +332,7 @@ class SettingsTab extends ConsumerWidget {
foregroundColor: theme.colorScheme.error,
minimumSize: const Size(48, 48),
),
onPressed: () =>
_confirmLogout(context, ref, unfinishedTasks),
onPressed: () => _confirmLogout(context, ref, unfinishedTasks),
child: Text(l10n.logout),
),
),
@@ -351,10 +359,7 @@ class _AccountAvatar extends StatelessWidget {
radius: 24,
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
child: Text(
initial,
style: Theme.of(context).textTheme.titleMedium,
),
child: Text(initial, style: Theme.of(context).textTheme.titleMedium),
);
}
}
+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,