From 7a5662c6cae6419bb66ed39959da2380de843ec0 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Thu, 28 Aug 2025 16:07:09 +0800 Subject: [PATCH] feat: login devices Closes #1140 Signed-off-by: bggRGjQaUbCoE --- lib/http/api.dart | 3 + lib/http/login.dart | 30 ++++++++ lib/models_new/login_devices/data.dart | 14 ++++ lib/models_new/login_devices/device.dart | 32 ++++++++ lib/pages/login_devices/controller.dart | 23 ++++++ lib/pages/login_devices/view.dart | 97 ++++++++++++++++++++++++ lib/pages/member/view.dart | 12 +++ 7 files changed, 211 insertions(+) create mode 100644 lib/models_new/login_devices/data.dart create mode 100644 lib/models_new/login_devices/device.dart create mode 100644 lib/pages/login_devices/controller.dart create mode 100644 lib/pages/login_devices/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 6aafea00..331502d6 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -948,4 +948,7 @@ class Api { static const String liveLikeReport = '${HttpString.liveBaseUrl}/xlive/app-ucenter/v1/like_info_v3/like/likeReportV3'; + + static const String loginDevices = + '${HttpString.passBaseUrl}/x/safecenter/user_login_devices'; } diff --git a/lib/http/login.dart b/lib/http/login.dart index 0dcabd66..def57cfc 100644 --- a/lib/http/login.dart +++ b/lib/http/login.dart @@ -3,7 +3,10 @@ import 'dart:convert'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/login/model.dart'; +import 'package:PiliPlus/models_new/login_devices/data.dart'; +import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/app_sign.dart'; import 'package:PiliPlus/utils/login_utils.dart'; @@ -499,4 +502,31 @@ class LoginHttp { ); return {'status': res.data['code'] == 0, 'msg': res.data['message']}; } + + static Future> loginDevices() async { + final account = Accounts.main; + final buvid = LoginUtils.buvid; + final params = { + 'local_id': buvid, + 'buvid': buvid, + 'device_name': 'android', + 'device_platform': 'android', + 'csrf': account.csrf, + 'mobi_app': 'android_hd', + 'platform': 'android', + 'access_key': account.accessKey, + 'statistics': Constants.statistics, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + AppSign.appSign(params); + var res = await Request().get( + Api.loginDevices, + queryParameters: params, + ); + if (res.data['code'] == 0) { + return Success(LoginDevicesData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models_new/login_devices/data.dart b/lib/models_new/login_devices/data.dart new file mode 100644 index 00000000..e9a911d9 --- /dev/null +++ b/lib/models_new/login_devices/data.dart @@ -0,0 +1,14 @@ +import 'package:PiliPlus/models_new/login_devices/device.dart'; + +class LoginDevicesData { + List? devices; + + LoginDevicesData({this.devices}); + + factory LoginDevicesData.fromJson(Map json) => + LoginDevicesData( + devices: (json['devices'] as List?) + ?.map((e) => LoginDevice.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/models_new/login_devices/device.dart b/lib/models_new/login_devices/device.dart new file mode 100644 index 00000000..69271119 --- /dev/null +++ b/lib/models_new/login_devices/device.dart @@ -0,0 +1,32 @@ +class LoginDevice { + int? mid; + String? localId; + String? deviceName; + String? devicePlatform; + bool? isCurrentDevice; + String? latestLoginAt; + String? source; + int? origin; + + LoginDevice({ + this.mid, + this.localId, + this.deviceName, + this.devicePlatform, + this.isCurrentDevice, + this.latestLoginAt, + this.source, + this.origin, + }); + + factory LoginDevice.fromJson(Map json) => LoginDevice( + mid: json['mid'] as int?, + localId: json['local_id'] as String?, + deviceName: json['device_name'] as String?, + devicePlatform: json['device_platform'] as String?, + isCurrentDevice: json['is_current_device'] as bool?, + latestLoginAt: json['latest_login_at'] as String?, + source: json['source'] as String?, + origin: json['origin'] as int?, + ); +} diff --git a/lib/pages/login_devices/controller.dart b/lib/pages/login_devices/controller.dart new file mode 100644 index 00000000..383070f0 --- /dev/null +++ b/lib/pages/login_devices/controller.dart @@ -0,0 +1,23 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/login.dart'; +import 'package:PiliPlus/models_new/login_devices/data.dart'; +import 'package:PiliPlus/models_new/login_devices/device.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; + +class LoginDevicesController + extends CommonListController { + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + List? getDataList(LoginDevicesData response) { + return response.devices; + } + + @override + Future> customGetData() => + LoginHttp.loginDevices(); +} diff --git a/lib/pages/login_devices/view.dart b/lib/pages/login_devices/view.dart new file mode 100644 index 00000000..6d8f369a --- /dev/null +++ b/lib/pages/login_devices/view.dart @@ -0,0 +1,97 @@ +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/login_devices/device.dart'; +import 'package:PiliPlus/pages/login_devices/controller.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LoginDevicesPage extends StatefulWidget { + const LoginDevicesPage({super.key}); + + @override + State createState() => LloginDevicesPageState(); +} + +class LloginDevicesPageState extends State { + final _controller = Get.put(LoginDevicesController()); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Scaffold( + appBar: AppBar(title: const Text('登录设备')), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 650), + child: refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + ViewSliverSafeArea( + sliver: MediaQuery.removeViewPadding( + context: context, + removeLeft: true, + removeRight: true, + child: Obx( + () => _buildBody( + colorScheme, + _controller.loadingState.value, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildBody( + ColorScheme colorScheme, + LoadingState?> loadingState, + ) { + late final divider = Divider( + height: 1, + color: colorScheme.outline.withValues(alpha: 0.1), + ); + return switch (loadingState) { + Loading() => const SliverToBoxAdapter(), + Success?>(:var response) => + response?.isNotEmpty == true + ? SliverList.separated( + itemBuilder: (context, index) { + return _buildItem(colorScheme, response[index]); + }, + itemCount: response!.length, + separatorBuilder: (_, _) => divider, + ) + : HttpError(onReload: _controller.onReload), + Error(:var errMsg) => HttpError( + errMsg: errMsg, + onReload: _controller.onReload, + ), + }; + } + + Widget _buildItem(ColorScheme colorScheme, LoginDevice item) { + final style = TextStyle(fontSize: 13, color: colorScheme.outline); + return ListTile( + dense: true, + title: Text( + item.deviceName ?? '', + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + '${item.latestLoginAt} ${item.source}', + style: style, + ), + trailing: item.isCurrentDevice == true + ? Text('(本机)', style: style) + : null, + ); + } +} diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 5b5e5828..38b245f8 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/models_new/space/space/data.dart'; import 'package:PiliPlus/pages/coin_log/controller.dart'; import 'package:PiliPlus/pages/exp_log/controller.dart'; import 'package:PiliPlus/pages/log_table/view.dart'; +import 'package:PiliPlus/pages/login_devices/view.dart'; import 'package:PiliPlus/pages/login_log/controller.dart'; import 'package:PiliPlus/pages/member/controller.dart'; import 'package:PiliPlus/pages/member/widget/user_info_card.dart'; @@ -190,6 +191,17 @@ class _MemberPageState extends State { ], ), ), + PopupMenuItem( + onTap: () => Get.to(const LoginDevicesPage()), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.devices, size: 18), + SizedBox(width: 10), + Text('登录设备'), + ], + ), + ), PopupMenuItem( onTap: () => Get.to( const LogPage(),