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:
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user