diff --git a/apps/mobile/lib/pages/albums_tab.dart b/apps/mobile/lib/pages/albums_tab.dart index 71bc0b9..1a38a3c 100644 --- a/apps/mobile/lib/pages/albums_tab.dart +++ b/apps/mobile/lib/pages/albums_tab.dart @@ -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(), ); diff --git a/apps/mobile/lib/pages/home_page.dart b/apps/mobile/lib/pages/home_page.dart index 7ede701..2b1cf96 100644 --- a/apps/mobile/lib/pages/home_page.dart +++ b/apps/mobile/lib/pages/home_page.dart @@ -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), ); } } diff --git a/apps/mobile/lib/pages/login_page.dart b/apps/mobile/lib/pages/login_page.dart index c125b1a..38e3b85 100644 --- a/apps/mobile/lib/pages/login_page.dart +++ b/apps/mobile/lib/pages/login_page.dart @@ -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 { 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 { 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 { _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 { 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 { 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 { 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 { 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,