From 6bab18a1bf7c4b2382a986ec36cc4b5be3bf8335 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 07:33:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E8=A2=AB=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E4=B8=8B=E7=BA=BF=E6=97=B6=E5=BC=B9=E5=AF=B9=E8=AF=9D=E6=A1=86?= =?UTF-8?q?=E5=91=8A=E7=9F=A5=E5=8E=9F=E5=9B=A0=E5=86=8D=E7=99=BB=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 会话轮询检测到 active=false → 先弹「此设备已被强制下线/需重新登录」对话框(沿用现有 AlertDialog 样式:surface 底/xl 圆角/accent 按钮),用户确认后再 logout 跳登录页, 不再默默跳走。_forcedLogoutHandling 防重复弹。新增 l10n sessionRevoked{Title,Body,Ok}。 Co-Authored-By: Claude Opus 4.8 --- client/lib/l10n/app_text.dart | 4 ++++ client/lib/l10n/strings_en.dart | 6 ++++++ client/lib/l10n/strings_zh.dart | 6 ++++++ client/lib/main.dart | 38 ++++++++++++++++++++++++++++----- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/client/lib/l10n/app_text.dart b/client/lib/l10n/app_text.dart index 41ee9d6..53dc9cb 100644 --- a/client/lib/l10n/app_text.dart +++ b/client/lib/l10n/app_text.dart @@ -108,6 +108,10 @@ abstract class AppText { String get devForceLogoutConfirm; // 弹窗正文(含设备名占位 %s) String get devClearLoginConfirm; String get devCancel; + // 本设备被其他设备强制下线时的提示框。 + String get sessionRevokedTitle; + String get sessionRevokedBody; + String get sessionRevokedOk; String get redeemEntry; String get contactEntry; String get signOut; diff --git a/client/lib/l10n/strings_en.dart b/client/lib/l10n/strings_en.dart index b932090..109a3b9 100644 --- a/client/lib/l10n/strings_en.dart +++ b/client/lib/l10n/strings_en.dart @@ -162,6 +162,12 @@ class StringsEn extends AppText { @override String get devCancel => 'Cancel'; @override + String get sessionRevokedTitle => 'This device was signed out'; + @override + String get sessionRevokedBody => 'Your account signed this device out from another device. Please sign in again.'; + @override + String get sessionRevokedOk => 'Sign in again'; + @override String get redeemEntry => 'Redeem & buy'; @override String get contactEntry => 'Contact us'; diff --git a/client/lib/l10n/strings_zh.dart b/client/lib/l10n/strings_zh.dart index e4bb09c..2defd44 100644 --- a/client/lib/l10n/strings_zh.dart +++ b/client/lib/l10n/strings_zh.dart @@ -161,6 +161,12 @@ class StringsZh extends AppText { @override String get devCancel => '取消'; @override + String get sessionRevokedTitle => '此设备已被强制下线'; + @override + String get sessionRevokedBody => '你的账号在其他设备上将此设备强制退出,需要重新登录。'; + @override + String get sessionRevokedOk => '重新登录'; + @override String get redeemEntry => '兑换 & 购买'; @override String get contactEntry => '联系我们'; diff --git a/client/lib/main.dart b/client/lib/main.dart index 064b3b8..72c3ca8 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -132,6 +132,7 @@ class _RootFlowState extends ConsumerState { bool _autoConnectArmed = false; // 自动连接每进程只尝试一次 bool _homeLogged = false; // 仅日志:HomeShell 首次显示打一行 bool _sessionWatchdogArmed = false; // 会话有效性轮询每进程只起一次 + bool _forcedLogoutHandling = false; // 正在弹「被强制下线」框 + 登出,避免重复 Timer? _sessionPoll; @override @@ -149,19 +150,46 @@ class _RootFlowState extends ConsumerState { } Future _checkSession() async { - if (!mounted || !ref.read(authProvider).isLoggedIn) return; + if (!mounted || _forcedLogoutHandling || !ref.read(authProvider).isLoggedIn) return; try { final deviceId = await ref.read(deviceIdentityProvider).deviceId(); final active = await ref.read(accountApiProvider).sessionActive(deviceId); - if (!active && mounted && ref.read(authProvider).isLoggedIn) { - logLine('Session', 'server revoked this device → logout'); - await ref.read(authProvider.notifier).logout(); - } + if (active || !mounted || !ref.read(authProvider).isLoggedIn) return; + // 被其他设备强制下线:先弹对话框告知原因,用户确认后再登出跳登录页。 + _forcedLogoutHandling = true; + logLine('Session', 'server revoked this device → notify + logout'); + await _showForcedLogoutDialog(); + if (mounted) await ref.read(authProvider.notifier).logout(); + _forcedLogoutHandling = false; } catch (_) { // 网络/鉴权异常不据此登出(fail-safe):只在服务端明确 active=false 才登出。 } } + Future _showForcedLogoutDialog() async { + if (!mounted) return; + final c = context.pangolin; + final t = ref.read(appTextProvider); + await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + backgroundColor: c.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.xl)), + title: Text(t.sessionRevokedTitle, + style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), + content: Text(t.sessionRevokedBody, style: PangolinText.sm.copyWith(color: c.fg2, height: 1.5)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(t.sessionRevokedOk, + style: PangolinText.sm.copyWith(color: c.accent, fontWeight: FontWeight.w700)), + ), + ], + ), + ); + } + // 进入登录态主界面后:开了「自动连接」且当前未连接,就自动连一次。 // 关键:必须同时等「设置已从持久化加载完(settings.loaded)」与「节点已就绪」—— // 二者都是异步且完成时刻接近,早读 autoConnect 会撞上还没加载完的默认 false(实测差 1ms)。