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,
|
onRetry: notifier.refresh,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return EmptyState(
|
// 空态也保留下拉刷新(被拉进相册后可手动刷新看到,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,
|
icon: Icons.photo_library_outlined,
|
||||||
title: l10n.albumsEmpty,
|
title: l10n.albumsEmpty,
|
||||||
actionLabel: l10n.albumCreate,
|
actionLabel: l10n.albumCreate,
|
||||||
onAction: () => AlbumsTab.openCreate(context, ref),
|
onAction: () => AlbumsTab.openCreate(context, ref),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +159,9 @@ class AlbumListBody extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 接近底部时预取下一页(loadMore 自带并发/末页守卫)。
|
// 接近底部时预取下一页(loadMore 自带并发/末页守卫)。
|
||||||
if (index >= rowCount - 2 && state.hasMore && !state.loadingMore) {
|
if (index >= rowCount - 2 &&
|
||||||
|
state.hasMore &&
|
||||||
|
!state.loadingMore) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
(_) => notifier.loadMore(),
|
(_) => notifier.loadMore(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../app/locale_controller.dart';
|
import '../app/locale_controller.dart';
|
||||||
import '../core/api_client.dart';
|
import '../core/api_client.dart';
|
||||||
|
import '../features/albums/providers.dart';
|
||||||
import '../features/uploads/engine_scope.dart';
|
import '../features/uploads/engine_scope.dart';
|
||||||
import '../features/uploads/queue_controller.dart';
|
import '../features/uploads/queue_controller.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
@@ -136,9 +137,7 @@ class SettingsTab extends ConsumerWidget {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
// 长文案/大字体下允许滚动,面板不做固定高度。
|
// 长文案/大字体下允许滚动,面板不做固定高度。
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(kSpace24, 0, kSpace24, kSpace24),
|
||||||
kSpace24, 0, kSpace24, kSpace24,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -206,6 +205,10 @@ class SettingsTab extends ConsumerWidget {
|
|||||||
await ref.read(apiClientProvider).logout();
|
await ref.read(apiClientProvider).logout();
|
||||||
// 会话账号失效 → 引擎 stop(任务保留库内,见 engine_scope.dart)。
|
// 会话账号失效 → 引擎 stop(任务保留库内,见 engine_scope.dart)。
|
||||||
await ref.read(sessionAccountProvider.notifier).refresh();
|
await ref.read(sessionAccountProvider.notifier).refresh();
|
||||||
|
// 账号级缓存失效(2026-09-23 用户报告:换账号后相册列表仍是上一账号数据
|
||||||
|
// ——首页 IndexedStack 三 tab 常驻不销毁,非 autoDispose 的列表 Provider
|
||||||
|
// 必须显式失效;任务库已按账号隔离键隔离,无需清)。
|
||||||
|
ref.invalidate(albumListProvider);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||||
}
|
}
|
||||||
@@ -226,7 +229,10 @@ class SettingsTab extends ConsumerWidget {
|
|||||||
// ── 账号摘要卡(只读展示,非编辑入口) ─────────────────
|
// ── 账号摘要卡(只读展示,非编辑入口) ─────────────────
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
kSpace16, kSpace8, kSpace16, kSpace8,
|
kSpace16,
|
||||||
|
kSpace8,
|
||||||
|
kSpace16,
|
||||||
|
kSpace8,
|
||||||
),
|
),
|
||||||
child: SectionHeader(title: l10n.settingsSectionAccount),
|
child: SectionHeader(title: l10n.settingsSectionAccount),
|
||||||
),
|
),
|
||||||
@@ -263,7 +269,10 @@ class SettingsTab extends ConsumerWidget {
|
|||||||
// ── 通用:语言 / 改密 / 上传帮助 / 版本 ──────────────────
|
// ── 通用:语言 / 改密 / 上传帮助 / 版本 ──────────────────
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
kSpace16, kSpace24, kSpace16, kSpace8,
|
kSpace16,
|
||||||
|
kSpace24,
|
||||||
|
kSpace16,
|
||||||
|
kSpace8,
|
||||||
),
|
),
|
||||||
child: SectionHeader(title: l10n.settingsSectionGeneral),
|
child: SectionHeader(title: l10n.settingsSectionGeneral),
|
||||||
),
|
),
|
||||||
@@ -323,8 +332,7 @@ class SettingsTab extends ConsumerWidget {
|
|||||||
foregroundColor: theme.colorScheme.error,
|
foregroundColor: theme.colorScheme.error,
|
||||||
minimumSize: const Size(48, 48),
|
minimumSize: const Size(48, 48),
|
||||||
),
|
),
|
||||||
onPressed: () =>
|
onPressed: () => _confirmLogout(context, ref, unfinishedTasks),
|
||||||
_confirmLogout(context, ref, unfinishedTasks),
|
|
||||||
child: Text(l10n.logout),
|
child: Text(l10n.logout),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -351,10 +359,7 @@ class _AccountAvatar extends StatelessWidget {
|
|||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: scheme.primary,
|
backgroundColor: scheme.primary,
|
||||||
foregroundColor: scheme.onPrimary,
|
foregroundColor: scheme.onPrimary,
|
||||||
child: Text(
|
child: Text(initial, style: Theme.of(context).textTheme.titleMedium),
|
||||||
initial,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../core/api_client.dart';
|
import '../core/api_client.dart';
|
||||||
|
import '../features/albums/providers.dart';
|
||||||
import '../core/app_exception.dart';
|
import '../core/app_exception.dart';
|
||||||
import '../core/error_l10n.dart';
|
import '../core/error_l10n.dart';
|
||||||
import '../features/uploads/engine_scope.dart';
|
import '../features/uploads/engine_scope.dart';
|
||||||
@@ -56,12 +57,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
try {
|
try {
|
||||||
await ref.read(apiClientProvider).login(
|
await ref
|
||||||
_accountController.text.trim(),
|
.read(apiClientProvider)
|
||||||
_passwordController.text,
|
.login(_accountController.text.trim(), _passwordController.text);
|
||||||
);
|
|
||||||
// 登录成功:刷新会话账号 → 引擎以新账号 scope start(Task 9 接线)。
|
// 登录成功:刷新会话账号 → 引擎以新账号 scope start(Task 9 接线)。
|
||||||
await ref.read(sessionAccountProvider.notifier).refresh();
|
await ref.read(sessionAccountProvider.notifier).refresh();
|
||||||
|
// 账号级缓存失效(换账号登录/会话过期重登同一保障:列表等 Provider
|
||||||
|
// 非 autoDispose,首页 IndexedStack 常驻——不失效会短暂显示上一账号数据)。
|
||||||
|
ref.invalidate(albumListProvider);
|
||||||
navigator.pushReplacementNamed('/home');
|
navigator.pushReplacementNamed('/home');
|
||||||
} on AppException catch (error) {
|
} on AppException catch (error) {
|
||||||
// 429(AUTH_RATE_LIMITED)等文案经错误码映射;登录页的
|
// 429(AUTH_RATE_LIMITED)等文案经错误码映射;登录页的
|
||||||
@@ -84,17 +87,16 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
top: false,
|
top: false,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final keyboardOpen =
|
final keyboardOpen = MediaQuery.of(context).viewInsets.bottom > 0;
|
||||||
MediaQuery.of(context).viewInsets.bottom > 0;
|
|
||||||
// 正常高度 28%(25%-32% 区间);键盘/短屏压缩至 10%。
|
// 正常高度 28%(25%-32% 区间);键盘/短屏压缩至 10%。
|
||||||
final headerHeight = (constraints.maxHeight *
|
final headerHeight =
|
||||||
(keyboardOpen ? 0.10 : 0.28))
|
(constraints.maxHeight * (keyboardOpen ? 0.10 : 0.28)).clamp(
|
||||||
.clamp(96.0, 320.0);
|
96.0,
|
||||||
|
320.0,
|
||||||
|
);
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
minHeight: constraints.maxHeight,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -102,18 +104,18 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
_CollageHeader(height: headerHeight),
|
_CollageHeader(height: headerHeight),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
kSpace24, kSpace24, kSpace24, kSpace24,
|
kSpace24,
|
||||||
|
kSpace24,
|
||||||
|
kSpace24,
|
||||||
|
kSpace24,
|
||||||
),
|
),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints:
|
constraints: const BoxConstraints(maxWidth: 460),
|
||||||
const BoxConstraints(maxWidth: 460),
|
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
autovalidateMode:
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
AutovalidateMode.onUserInteraction,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
_BrandRow(),
|
_BrandRow(),
|
||||||
const SizedBox(height: kSpace16),
|
const SizedBox(height: kSpace16),
|
||||||
@@ -136,15 +138,13 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
const SizedBox(height: kSpace24),
|
const SizedBox(height: kSpace24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _accountController,
|
controller: _accountController,
|
||||||
autofillHints: const [
|
autofillHints: const [AutofillHints.username],
|
||||||
AutofillHints.username,
|
|
||||||
],
|
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.username,
|
labelText: l10n.username,
|
||||||
),
|
),
|
||||||
validator: (value) => (value == null ||
|
validator: (value) =>
|
||||||
value.trim().isEmpty)
|
(value == null || value.trim().isEmpty)
|
||||||
? l10n.fieldRequired
|
? l10n.fieldRequired
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -154,9 +154,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
autofillHints: const [
|
autofillHints: const [AutofillHints.password],
|
||||||
AutofillHints.password,
|
|
||||||
],
|
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onFieldSubmitted: (_) =>
|
onFieldSubmitted: (_) =>
|
||||||
_submitting ? null : _submit(),
|
_submitting ? null : _submit(),
|
||||||
@@ -174,15 +172,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
_obscurePassword
|
_obscurePassword
|
||||||
? Icons.visibility_outlined
|
? Icons.visibility_outlined
|
||||||
: Icons
|
: Icons.visibility_off_outlined,
|
||||||
.visibility_off_outlined,
|
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: (value) => (value == null ||
|
validator: (value) =>
|
||||||
value.isEmpty)
|
(value == null || value.isEmpty)
|
||||||
? l10n.fieldRequired
|
? l10n.fieldRequired
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -211,8 +208,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
l10n.loginAskAdmin,
|
l10n.loginAskAdmin,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color:
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -267,20 +263,32 @@ class _CollageHeader extends StatelessWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
left: 28,
|
left: 28,
|
||||||
top: 24,
|
top: 24,
|
||||||
child: _tile(120, 84, Colors.white.withValues(alpha: 0.16),
|
child: _tile(
|
||||||
rotate: -0.08),
|
120,
|
||||||
|
84,
|
||||||
|
Colors.white.withValues(alpha: 0.16),
|
||||||
|
rotate: -0.08,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 44,
|
right: 44,
|
||||||
top: 40,
|
top: 40,
|
||||||
child: _tile(132, 92, Colors.white.withValues(alpha: 0.12),
|
child: _tile(
|
||||||
rotate: 0.06),
|
132,
|
||||||
|
92,
|
||||||
|
Colors.white.withValues(alpha: 0.12),
|
||||||
|
rotate: 0.06,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 96,
|
left: 96,
|
||||||
bottom: 18,
|
bottom: 18,
|
||||||
child: _tile(150, 72, Colors.white.withValues(alpha: 0.10),
|
child: _tile(
|
||||||
rotate: -0.04),
|
150,
|
||||||
|
72,
|
||||||
|
Colors.white.withValues(alpha: 0.10),
|
||||||
|
rotate: -0.04,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// 右上语言选择(浮于拼贴之上)。
|
// 右上语言选择(浮于拼贴之上)。
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -293,8 +301,7 @@ class _CollageHeader extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _tile(double width, double height, Color color,
|
Widget _tile(double width, double height, Color color, {double rotate = 0}) {
|
||||||
{double rotate = 0}) {
|
|
||||||
return Transform.rotate(
|
return Transform.rotate(
|
||||||
angle: rotate,
|
angle: rotate,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -349,8 +356,7 @@ class _LanguagePill extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final isZh =
|
final isZh = Localizations.maybeLocaleOf(context)?.languageCode == 'zh';
|
||||||
Localizations.maybeLocaleOf(context)?.languageCode == 'zh';
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: l10n.language,
|
label: l10n.language,
|
||||||
button: true,
|
button: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user