feat(client): 设备数超限挡板 DeviceLimitScreen(#16 前端)
登录响应带 device_limit 时,顶层路由把用户挡在「移除设备」页,降到上限内才进主界面。 - auth_api.dart:AuthTokens 解析 device_limit;新增 DeviceLimit/DeviceBrief 模型 - auth_provider.dart:deviceLimitProvider(挡板状态);auth_screen 登录后置入 - main.dart:isLoggedIn 后若 deviceLimit!=null → DeviceLimitScreen(挡 HomeShell) - screens/device_limit_screen.dart(新):设备列表(本机标注不可删)+ 每台移除 + 一键「移除最久未用」+ 退出登录;降到上限内自动清挡板放行(复用 devicesProvider.remove) - l10n:deviceLimitTitle/Desc/RemoveOldest(zh/en);顺带修 devicesSub 文案 5→3 台 analyze 干净;client 全量测试 161 通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,14 +26,63 @@ class AuthApiException implements Exception {
|
||||
|
||||
/// 登录 / 注册成功后返回的 JWT 令牌对。
|
||||
class AuthTokens {
|
||||
const AuthTokens({required this.accessToken, required this.refreshToken});
|
||||
const AuthTokens({
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
this.deviceLimit,
|
||||
});
|
||||
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
|
||||
/// 非 null 表示登录成功但账户活跃设备数超套餐上限——登录仍生效(token 已发),
|
||||
/// 客户端须先让用户移除设备(降到上限内)才进主界面。见 device_limit 挡板。
|
||||
final DeviceLimit? deviceLimit;
|
||||
|
||||
factory AuthTokens.fromJson(Map<String, dynamic> m) => AuthTokens(
|
||||
accessToken: m['access_token'] as String? ?? '',
|
||||
refreshToken: m['refresh_token'] as String? ?? '',
|
||||
deviceLimit: m['device_limit'] is Map
|
||||
? DeviceLimit.fromJson(Map<String, dynamic>.from(m['device_limit'] as Map))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 超限信号:当前套餐上限 + 账户活跃设备列表(供「移除设备」页展示)。
|
||||
class DeviceLimit {
|
||||
const DeviceLimit({required this.maxDevices, required this.devices});
|
||||
|
||||
final int maxDevices;
|
||||
final List<DeviceBrief> devices;
|
||||
|
||||
factory DeviceLimit.fromJson(Map<String, dynamic> m) => DeviceLimit(
|
||||
maxDevices: (m['max_devices'] as num?)?.toInt() ?? 0,
|
||||
devices: ((m['devices'] as List?) ?? const [])
|
||||
.whereType<Map>()
|
||||
.map((e) => DeviceBrief.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 超限页用的精简设备视图。
|
||||
class DeviceBrief {
|
||||
const DeviceBrief({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.platform,
|
||||
this.lastSeen,
|
||||
});
|
||||
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String platform;
|
||||
final String? lastSeen; // RFC 3339 UTC
|
||||
|
||||
factory DeviceBrief.fromJson(Map<String, dynamic> m) => DeviceBrief(
|
||||
uuid: m['uuid'] as String? ?? '',
|
||||
name: m['name'] as String? ?? '',
|
||||
platform: m['platform'] as String? ?? '',
|
||||
lastSeen: m['last_seen'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user