完善游客模式登录 并且新增设备管理

This commit is contained in:
2025-10-17 21:11:40 +08:00
parent 2333ad52a9
commit de9cf751f3
46 changed files with 4590 additions and 543 deletions
@@ -65,7 +65,7 @@ class KRCrispController extends GetxController {
try {
final appData = KRAppRunData();
final currentLanguage = KRLanguageUtils.getCurrentLanguageCode();
final userEmail = appData.kr_account ?? '';
final userEmail = appData.kr_account.value ?? '';
// 获取设备 ID
final deviceId = await KRDeviceUtil().kr_getDeviceId();
@@ -43,23 +43,17 @@ class KRDeleteAccountController extends GetxController {
super.onClose();
}
// 发送验证码
// 发送验证码(仅支持邮箱)
Future<void> kr_sendCode() async {
final account = KRAppRunData.getInstance().kr_account;
final account = KRAppRunData.getInstance().kr_account.value;
if (account == null || account.isEmpty) {
KRCommonUtil.kr_showToast('账号不能为空');
return;
}
// 判断账号类型
final isEmail = KRAppRunData.getInstance().kr_loginType == KRLoginType.kr_email;
final type = isEmail ? KRLoginType.kr_email : KRLoginType.kr_telephone;
// 发送验证码
// 发送验证码(简化后的 API 只需要 email 和 type
final result = await _authApi.kr_sendCode(
type,
account,
KRAppRunData.getInstance().kr_areaCode, // 手机号不需要区号
account, // 邮箱地址
2, // 删除账号的验证码类型
);
@@ -70,7 +64,7 @@ class KRDeleteAccountController extends GetxController {
(success) {
kr_canSendCode.value = false;
kr_countdown.value = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (kr_countdown.value > 0) {
kr_countdown.value--;
@@ -89,11 +83,12 @@ class KRDeleteAccountController extends GetxController {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.sendCode);
return;
}
final result = await _authApi.kr_deleteAccount(
KRAppRunData.getInstance().kr_loginType ?? KRLoginType.kr_telephone,
// 删除账号(简化后的 API 只需要 code)
final result = await _authApi.kr_deleteAccount(
kr_codeController.text,
);
result.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
@@ -103,7 +98,5 @@ class KRDeleteAccountController extends GetxController {
KRAppRunData.getInstance().kr_loginOut();
},
);
// TODO: 实现删除账号的逻辑
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRDeviceManagementController>(
() => KRDeviceManagementController(),
);
}
}
@@ -0,0 +1,294 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/services/kr_device_info_service.dart';
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'dart:io';
import 'dart:math';
class KRDeviceManagementController extends GetxController {
// 设备列表
final RxList<Map<String, dynamic>> devices = <Map<String, dynamic>>[].obs;
// 加载状态
final RxBool isLoading = true.obs;
// 当前设备ID
String? currentDeviceId;
@override
void onInit() {
super.onInit();
_initDeviceId();
loadDeviceList();
}
/// 初始化当前设备ID
Future<void> _initDeviceId() async {
try {
currentDeviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('当前设备ID: $currentDeviceId', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceManagement');
}
}
/// 加载设备列表
Future<void> loadDeviceList() async {
try {
isLoading.value = true;
KRLogUtil.kr_i('开始加载设备列表', tag: 'DeviceManagement');
// 调用API获取设备列表
final result = await KRUserApi().kr_getUserDevices();
result.fold(
(error) {
KRLogUtil.kr_e('加载设备列表失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar('错误', error.msg);
},
(deviceList) {
KRLogUtil.kr_i('获取到 ${deviceList.length} 个设备', tag: 'DeviceManagement');
// 转换设备数据格式
devices.value = deviceList.map((device) {
final identifier = device['identifier']?.toString() ?? '';
final isCurrent = identifier == currentDeviceId;
return {
'id': device['id']?.toString() ?? '',
'identifier': identifier,
'device_name': device['user_agent'] ?? '未知设备',
'ip': device['ip'] ?? '',
'last_login': device['updated_at'] ?? device['created_at'] ?? '',
'is_current': isCurrent,
'enabled': device['enabled'] ?? true,
'online': device['online'] ?? false,
};
}).toList();
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar('错误', '加载设备列表失败');
} finally {
isLoading.value = false;
}
}
/// 删除设备
Future<void> deleteDevice(String id) async {
try {
// 检查是否是本机设备
final device = devices.firstWhere(
(d) => d['id'] == id,
orElse: () => {},
);
if (device.isEmpty) return;
final isCurrent = device['is_current'] ?? false;
// 使用响应式变量来接收确认结果
bool? confirmed;
// 显示确认对话框
await KRDialog.show(
title: '确认删除',
message: isCurrent
? '确定要删除本机设备吗?删除后将使用设备登录自动重新登录。'
: '确定要删除此设备吗?删除后该设备将被强制下线。',
icon: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_rounded,
color: Colors.red,
size: 32,
),
),
confirmText: '删除',
cancelText: '取消',
onConfirm: () {
confirmed = true;
},
onCancel: () {
confirmed = false;
},
);
if (confirmed != true) return;
KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement');
// 调用API解绑设备
final result = await KRUserApi().kr_unbindUserDevice(id);
result.fold(
(error) {
KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar('错误', '删除失败:${error.msg}');
},
(_) async {
KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement');
if (isCurrent) {
// 如果删除的是本机设备,重新进行设备登录
KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement');
// 先关闭当前设备管理页面
Get.back();
// 执行重新登录
await _reloginWithDevice();
} else {
// 删除其他设备,从列表中移除
devices.removeWhere((device) => device['id'] == id);
Get.snackbar('成功', '设备已删除');
}
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar('错误', '删除失败:$e');
}
}
/// 重新使用设备登录
Future<void> _reloginWithDevice() async {
try {
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
KRLogUtil.kr_i('开始重新进行设备登录', tag: 'DeviceManagement');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
// 先清除当前的用户信息(但不调用 kr_loginOut,避免显示登录界面)
final appRunData = KRAppRunData.getInstance();
appRunData.kr_isLogin.value = false;
appRunData.kr_token = null;
appRunData.kr_account.value = null;
appRunData.kr_userId.value = null;
// 检查是否启用设备登录
final siteConfigService = KRSiteConfigService();
final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled();
if (!isDeviceLoginEnabled) {
KRLogUtil.kr_w('设备登录未启用,执行完整退出登录', tag: 'DeviceManagement');
Get.snackbar('提示', '设备登录未启用,请手动登录');
await appRunData.kr_loginOut();
return;
}
KRLogUtil.kr_i('设备登录已启用,开始调用设备登录接口', tag: 'DeviceManagement');
// 初始化设备信息服务(如果还没初始化)
await KRDeviceInfoService().initialize();
// 调用设备登录接口
final authApi = KRAuthApi();
final result = await authApi.kr_deviceLogin();
result.fold(
(error) {
// 设备登录失败
KRLogUtil.kr_e('设备登录失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar('错误', '自动登录失败:${error.msg},请手动登录');
// 执行完整退出登录,显示登录界面
appRunData.kr_loginOut();
},
(token) async {
// 设备登录成功
KRLogUtil.kr_i('✅ 设备登录成功!', tag: 'DeviceManagement');
KRLogUtil.kr_i('🎫 Token: ${token.substring(0, min(20, token.length))}...', tag: 'DeviceManagement');
// 保存新的用户信息
final deviceId = KRDeviceInfoService().deviceId ?? 'unknown';
await appRunData.kr_saveUserInfo(
token,
'device_$deviceId',
KRLoginType.kr_email,
null,
);
KRLogUtil.kr_i('✅ 设备重新登录成功,已更新用户信息', tag: 'DeviceManagement');
// 等待一小段时间,确保登录状态已经更新
await Future.delayed(const Duration(milliseconds: 300));
// 刷新订阅信息
KRLogUtil.kr_i('🔄 开始刷新订阅信息...', tag: 'DeviceManagement');
try {
await KRSubscribeService().kr_refreshAll();
KRLogUtil.kr_i('✅ 订阅信息刷新成功', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('订阅信息刷新失败: $e', tag: 'DeviceManagement');
}
Get.snackbar('成功', '已自动重新登录');
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar('错误', '自动登录失败,请手动登录');
// 发生异常,执行完整退出登录
await KRAppRunData.getInstance().kr_loginOut();
}
}
/// 获取设备类型和图标
Map<String, dynamic> getDeviceTypeInfo(String userAgent) {
String deviceType = '未知设备';
String iconName = 'devices';
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
deviceType = '安卓设备';
iconName = 'phone_android';
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
deviceType = 'iOS 设备';
iconName = 'phone_iphone';
} else if (userAgent.contains('iPad')) {
deviceType = 'iPad';
iconName = 'tablet';
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
deviceType = 'macOS';
iconName = 'desktop_mac';
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
deviceType = 'Windows';
iconName = 'computer';
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
deviceType = 'Linux';
iconName = 'computer';
}
return {
'type': deviceType,
'icon': iconName,
};
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
}
@@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementView extends GetView<KRDeviceManagementController> {
const KRDeviceManagementView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15),
Color.fromRGBO(23, 151, 255, 0.05),
],
stops: [0.0, 0.28],
),
),
child: Column(
children: [
// 顶部导航栏
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Theme.of(context).iconTheme.color,
size: 20.w,
),
onPressed: () => Get.back(),
),
title: Text(
'设备管理',
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
// 内容区域
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(),
);
}
if (controller.devices.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.devices_other,
size: 64.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
SizedBox(height: 16.w),
Text(
'暂无登录设备',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () => controller.loadDeviceList(),
child: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: controller.devices.length,
itemBuilder: (context, index) {
return _buildDeviceItem(
context,
controller.devices[index],
);
},
),
);
}),
),
],
),
),
);
}
/// 构建设备项
Widget _buildDeviceItem(
BuildContext context, Map<String, dynamic> device) {
final id = device['id'] ?? '';
final identifier = device['identifier'] ?? '';
final userAgent = device['device_name'] ?? '未知设备';
final isCurrent = device['is_current'] ?? false;
final ip = device['ip'] ?? '';
final lastLoginRaw = device['last_login'];
final String lastLogin = lastLoginRaw?.toString() ?? '';
// 获取设备类型信息
final deviceInfo = controller.getDeviceTypeInfo(userAgent);
final deviceType = deviceInfo['type'] as String;
final iconName = deviceInfo['icon'] as String;
return Container(
margin: EdgeInsets.only(bottom: 12.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10.w,
offset: Offset(0, 2.w),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 设备类型和操作按钮
Row(
children: [
// 设备图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Icon(
_getIconData(iconName),
color: const Color(0xFF1797FF),
size: 24.w,
),
),
SizedBox(width: 12.w),
// 设备信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
deviceType,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
if (isCurrent) ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.w,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
'本机',
style: KrAppTextStyle(
fontSize: 10,
color: Colors.green,
),
),
),
],
],
),
SizedBox(height: 4.w),
Text(
'ID: ${identifier.substring(0, identifier.length > 12 ? 12 : identifier.length)}...',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
// 删除按钮
TextButton(
onPressed: () => controller.deleteDevice(id),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
),
child: Text(
'删除',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
// 分隔线
if (ip.isNotEmpty || lastLogin.isNotEmpty) ...[
SizedBox(height: 12.w),
Divider(height: 1, color: Theme.of(context).dividerColor),
SizedBox(height: 12.w),
],
// 详细信息
if (ip.isNotEmpty)
_buildInfoRow(
context,
'IP地址',
ip,
),
if (ip.isNotEmpty && lastLogin.isNotEmpty) SizedBox(height: 8.w),
if (lastLogin.isNotEmpty)
_buildInfoRow(
context,
'最后登录',
_formatDateTime(lastLoginRaw),
),
],
),
);
}
/// 构建信息行
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Row(
children: [
Text(
'$label: ',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
Expanded(
child: Text(
value,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
/// 格式化时间
String _formatDateTime(dynamic timestamp) {
if (timestamp == null) return '未知';
try {
DateTime dateTime;
if (timestamp is int) {
// 判断是秒级时间戳(10位)还是毫秒级时间戳(13位)
if (timestamp > 9999999999) {
// 毫秒级时间戳
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
} else {
// 秒级时间戳
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
} else if (timestamp is String) {
dateTime = DateTime.parse(timestamp);
} else {
return '未知';
}
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} catch (e) {
return '未知';
}
}
/// 获取图标数据
IconData _getIconData(String iconName) {
switch (iconName) {
case 'phone_android':
return Icons.phone_android;
case 'phone_iphone':
return Icons.phone_iphone;
case 'tablet':
return Icons.tablet_mac;
case 'desktop_mac':
return Icons.desktop_mac;
case 'computer':
return Icons.computer;
default:
return Icons.devices;
}
}
}
@@ -183,10 +183,7 @@ class KRHomeController extends GetxController {
if (isValidLogin) {
kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn;
KRLogUtil.kr_i('设置为已登录状态', tag: 'HomeController');
// 检查公告服务
KRAnnouncementService().kr_checkAnnouncement();
// 确保订阅服务初始化
_kr_ensureSubscribeServiceInitialized();
} else {
@@ -255,7 +252,15 @@ class KRHomeController extends GetxController {
} else {
kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn;
KRLogUtil.kr_i('登录状态变化:设置为未登录', tag: 'HomeController');
// 重置列表状态,防止出现无限高度
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
// 退出登录清理订阅服务
kr_subscribeService.kr_logout();
// 显式更新底部面板高度,确保未登录状态下高度正确
kr_updateBottomPanelHeight();
}
} catch (e) {
KRLogUtil.kr_e('处理登录状态变化失败: $e', tag: 'HomeController');
@@ -26,30 +26,13 @@ class KRHomeConnectionOptionsView extends GetView<KRHomeController> {
),
),
SizedBox(height: 8.w),
Row(
children: [
Flexible(
child: _buildConnectionOption(
"home_server",
AppTranslations.kr_home.dedicatedServers,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_serverList);
},
),
),
SizedBox(width: 12.w),
Flexible(
child: _buildConnectionOption(
"home_ct",
AppTranslations.kr_home.countryRegion,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList);
},
),
),
],
_buildConnectionOption(
"home_ct",
AppTranslations.kr_home.countryRegion,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList);
},
),
],
);
+25 -21
View File
@@ -31,28 +31,32 @@ class KRHomeView extends GetView<KRHomeController> {
// 地图视图
const KRHomeMapView(),
// 登录视图
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, -2),
),
],
Align(
alignment: Alignment.bottomCenter,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: const KRLoginView(),
),
child: const KRLoginView(),
),
),
],
@@ -71,16 +71,15 @@ class KRInviteController extends GetxController {
}
}
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 邀请码现在使用 AppConfig 中的固定值,等待新接口实现
Future<void> _kr_fetchUserInfo() async {
try {
kr_isLoading.value = true;
final either = await KRUserApi().kr_getUserInfo();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(userInfo) {
kr_referCode.value = userInfo.referCode;
},
);
// 使用 AppConfig 中的固定邀请码
kr_referCode.value = AppConfig.kr_userReferCode;
} catch (e) {
KRCommonUtil.kr_showToast(e.toString());
} finally {
@@ -9,6 +9,7 @@ import 'package:kaer_with_panels/app/model/kr_area_code.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_event_bus.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import '../../../localization/kr_language_utils.dart';
@@ -125,9 +126,9 @@ class KRLoginController extends GetxController
String kr_getNextBtnText() {
switch (kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
return AppTranslations.kr_login.next;
return AppTranslations.kr_login.passwordLogin; // 显示"登录"
case KRLoginProgressStatus.kr_loginByCode:
return AppTranslations.kr_login.codeLogin;
return AppTranslations.kr_login.passwordLogin; // 已废弃,保留以防兼容性问题
case KRLoginProgressStatus.kr_loginByPsd:
return AppTranslations.kr_login.passwordLogin;
case KRLoginProgressStatus.kr_registerSendCode:
@@ -198,25 +199,17 @@ class KRLoginController extends GetxController
}
});
// 修改 accountController 监听器
// accountController 监听器(仅支持邮箱)
accountController.addListener(() {
String input = accountController.text.trim();
// 延迟执行状态更新,避免在输入过程中频繁切换
// 延迟执行状态更新
Future.microtask(() {
final isNumeric = _isNumeric(input);
if (isNumeric && kr_loginType.value != KRLoginType.kr_telephone) {
kr_loginType.value = KRLoginType.kr_telephone;
kr_emailList.clear();
kr_removeOverlay();
} else if (!isNumeric && kr_loginType.value != KRLoginType.kr_email) {
kr_loginType.value = KRLoginType.kr_email;
}
// 始终保持邮箱类型
kr_loginType.value = KRLoginType.kr_email;
// 只在邮箱模式下更新邮箱列表
if (!isNumeric) {
kr_emailList.value = kr_generateAndSortEmailList(input);
}
// 更新邮箱建议列表
kr_emailList.value = kr_generateAndSortEmailList(input);
kr_accountHasText.value = input.isNotEmpty;
});
@@ -259,40 +252,29 @@ class KRLoginController extends GetxController
return numericRegex.hasMatch(input);
}
/// 检查是否注册
/// 直接登录(不再检查是否注册
void kr_check() async {
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
return;
}
final either = await KRAuthApi().kr_isRegister(
kr_loginType.value,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
kr_isRegistered.value = r;
kr_loginStatus.value = r
? KRLoginProgressStatus.kr_loginByPsd
: KRLoginProgressStatus.kr_registerSendCode;
});
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
// 直接调用登录
kr_login();
}
/// 发送验证码
/// 发送验证码(仅支持邮箱)
void kr_sendCode() async {
final either = await KRAuthApi().kr_sendCode(
kr_loginType.value,
accountController.text,
kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode,
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 1
: kr_loginStatus.value == KRLoginProgressStatus.kr_forgetPsdSendCode
? 2
: 2);
? 2 // 注册验证码类型为2
: 3); // 重置密码验证码类型为3
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
@@ -301,28 +283,15 @@ class KRLoginController extends GetxController
});
}
/// 开始登录
/// 开始登录(仅支持邮箱+密码)
void kr_login() async {
if (kr_loginStatus == KRLoginProgressStatus.kr_loginByCode &&
codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
return;
}
if (kr_loginStatus == KRLoginProgressStatus.kr_loginByPsd &&
psdController.text.isEmpty) {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
final either = await KRAuthApi().kr_login(
kr_loginType.value,
kr_loginStatus.value == KRLoginProgressStatus.kr_loginByPsd,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null,
codeController.text,
psdController.text);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
@@ -331,8 +300,14 @@ class KRLoginController extends GetxController
});
}
/// 开始注册
/// 开始注册(仅支持邮箱,验证码和邀请码可选)
void kr_register() async {
// 验证邮箱
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
return;
}
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
@@ -346,20 +321,26 @@ class KRLoginController extends GetxController
return;
}
// 检查是否需要验证码(基于站点配置)
final siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
if (needVerification && codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
return;
}
final either = await KRAuthApi().kr_register(
kr_loginType.value,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null,
codeController.text,
psdController.text,
inviteCode: inviteCodeController.text);
code: codeController.text.isEmpty ? null : codeController.text,
inviteCode: inviteCodeController.text.isEmpty ? null : inviteCodeController.text);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
_saveLoginData(r);
KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
});
}
@@ -390,20 +371,17 @@ class KRLoginController extends GetxController
}
}
/// 验证验证码
/// 验证验证码(仅支持邮箱)
void kr_checkVerificationCode(KRLoginProgressStatus status) async {
final either = await KRAuthApi().kr_checkVerificationCode(
kr_loginType.value,
accountController.text,
kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode,
codeController.text,
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 1
: 2);
? 2 // 注册验证码类型为2
: 3); // 重置密码验证码类型为3
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
if (status == KRLoginProgressStatus.kr_registerSendCode) {
kr_loginStatus.value = KRLoginProgressStatus.kr_registerSetPsd;
} else if (status == KRLoginProgressStatus.kr_forgetPsdSendCode) {
@@ -412,7 +390,7 @@ class KRLoginController extends GetxController
});
}
/// 忘记密码--- 设置新密码
/// 忘记密码-设置新密码(仅支持邮箱)
void kr_setNewPsdByForgetPsd() async {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
@@ -428,11 +406,7 @@ class KRLoginController extends GetxController
}
final either = await KRAuthApi().kr_setNewPsdByForgetPsd(
kr_loginType.value,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null,
codeController.text,
psdController.text);
either.fold((l) {
@@ -464,15 +438,13 @@ class KRLoginController extends GetxController
});
}
/// 设置登录数据
/// 设置登录数据(仅支持邮箱)
void _saveLoginData(String token) {
KRAppRunData.getInstance().kr_saveUserInfo(
token,
accountController.text,
kr_loginType.value,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null);
KRLoginType.kr_email,
null);
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
// 登录/注册成功后,发送消息触发订阅服务刷新
@@ -480,6 +452,9 @@ class KRLoginController extends GetxController
Future.delayed(Duration(milliseconds: 100), () {
KREventBus().kr_sendMessage(KRMessageType.kr_payment);
});
// 登录成功后返回到上一页
Get.back();
}
/// 根据输入内容匹配邮箱
@@ -611,6 +586,14 @@ class KRLoginController extends GetxController
// }
}
@override
void onReady() {
super.onReady();
// 每次打开登录页面时,重置为登录状态(而不是注册状态)
// 这样确保点击"登录/注册"按钮时始终显示登录页面
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
}
@override
void onClose() {
kr_removeOverlay();
@@ -640,13 +623,9 @@ class KRLoginController extends GetxController
void _updateInputState() {
String input = accountController.text.trim();
if (_isNumeric(input)) {
kr_loginType.value = KRLoginType.kr_telephone;
kr_emailList.clear();
} else {
kr_loginType.value = KRLoginType.kr_email;
kr_emailList.value = kr_generateAndSortEmailList(input);
}
// 始终保持邮箱类型
kr_loginType.value = KRLoginType.kr_email;
kr_emailList.value = kr_generateAndSortEmailList(input);
}
// 添加移除悬浮框的方法
+435 -46
View File
@@ -14,6 +14,7 @@ import '../controllers/kr_login_controller.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
class KRLoginView extends GetView<KRLoginController> {
const KRLoginView({super.key});
@@ -21,15 +22,16 @@ class KRLoginView extends GetView<KRLoginController> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
onTap: () {
if (!FocusScope.of(context).hasPrimaryFocus) {
FocusScope.of(context).unfocus();
}
_hideDropdown();
},
child: Container(
color: theme.scaffoldBackgroundColor,
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (!FocusScope.of(context).hasPrimaryFocus) {
FocusScope.of(context).unfocus();
}
_hideDropdown();
},
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Obx(() {
@@ -58,16 +60,18 @@ class KRLoginView extends GetView<KRLoginController> {
}
Widget _buildCheckView(BuildContext context) {
// 构建检查视图的代码
// 构建登录视图 - 直接显示邮箱和密码输入框
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
// 总是显示返回按钮,包括 kr_check 状态
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
// 邮箱输入框
_buildInputSection(context, Theme.of(context)),
SizedBox(height: 8.w),
// 密码输入框
_buildPasswordInput(Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
_buildNextButton(controller.kr_getNextBtnText(), Theme.of(context)),
SizedBox(height: _getBottomPadding()),
@@ -75,6 +79,63 @@ class KRLoginView extends GetView<KRLoginController> {
);
}
/// 构建密码输入框(专用于 kr_check 状态)
Widget _buildPasswordInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_psd"),
),
SizedBox(width: 8.w),
Expanded(
child: Obx(() => TextField(
controller: controller.psdController,
obscureText: controller.kr_obscureText.value,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: '请输入密码',
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
suffixIcon: controller.kr_psdHasText.value
? IconButton(
icon: Icon(
controller.kr_obscureText.value
? Icons.visibility_off
: Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: () {
controller.kr_obscureText.value = !controller.kr_obscureText.value;
},
)
: null,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
)),
),
],
),
);
}
/// 获取底部间距
double _getBottomPadding() {
switch (controller.kr_loginStatus.value) {
@@ -98,10 +159,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
@@ -116,10 +174,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
@@ -130,18 +185,29 @@ class KRLoginView extends GetView<KRLoginController> {
}
Widget _buildRegisterSendCodeView(BuildContext context) {
// 构建注册发送验证码视图的代码
// 构建单页注册视图 - 所有字段显示在一个页面
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
_buildNextButton(controller.kr_getNextBtnText(), Theme.of(context)),
_buildBackButton(theme),
_buildHeaderSection(theme),
// 邮箱输入框
_buildRegistrationEmailInput(theme),
SizedBox(height: 8.w),
// 验证码输入框(根据站点配置决定是否显示,包括底部间距)
_buildRegistrationCodeInputWithSpacing(theme),
// 密码输入框
_buildRegistrationPasswordInput(theme),
SizedBox(height: 8.w),
// 确认密码输入框
_buildRegistrationConfirmPasswordInput(theme),
SizedBox(height: 8.w),
// 邀请码输入框(可选)
_buildInviteCodeInput(theme),
SizedBox(height: 17.w),
// 注册按钮
_buildNextButton('注册账号', theme),
SizedBox(height: _getBottomPadding()),
],
);
@@ -152,10 +218,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
Column(
@@ -178,10 +241,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
@@ -196,10 +256,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_forgetPsdSetPsd)
@@ -670,7 +727,8 @@ class KRLoginView extends GetView<KRLoginController> {
Widget? content;
switch (controller.kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
content = _buildAgreementText(theme);
// 在登录状态显示"忘记密码"和切换按钮
content = _buildLoginBtns(theme);
case KRLoginProgressStatus.kr_loginByCode:
case KRLoginProgressStatus.kr_loginByPsd:
content = _buildLoginBtns(theme);
@@ -744,6 +802,34 @@ class KRLoginView extends GetView<KRLoginController> {
/// 构建登录按钮行
Widget _buildLoginBtns(ThemeData theme) {
// 在 kr_check 状态下显示不同的按钮
if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_check) {
return SizedBox(
height: 24.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildHoverableTextButton(
text: 'login.forgotPassword'.tr,
onTap: () => controller.kr_loginStatus.value =
KRLoginProgressStatus.kr_forgetPsdSendCode,
theme: theme,
),
SizedBox(width: 16.w),
_buildHoverableTextButton(
text: '点我注册',
onTap: () {
// 跳转到注册页面
controller.kr_loginStatus.value = KRLoginProgressStatus.kr_registerSendCode;
},
theme: theme,
),
],
),
);
}
// 其他状态保持原有逻辑
return SizedBox(
height: 24.w,
child: Row(
@@ -802,7 +888,8 @@ class KRLoginView extends GetView<KRLoginController> {
controller.kr_login();
break;
case KRLoginProgressStatus.kr_registerSendCode:
controller.kr_checkCode();
// 直接调用注册,不再需要多步骤
controller.kr_register();
break;
case KRLoginProgressStatus.kr_registerSetPsd:
controller.kr_register();
@@ -981,8 +1068,21 @@ class KRLoginView extends GetView<KRLoginController> {
Widget _buildBackButton(ThemeData theme) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
controller.kr_back();
print('👆 返回按钮被点击了!');
print('👆 当前登录状态: ${controller.kr_loginStatus.value}');
// 只有在初始登录状态(kr_check)时,返回按钮才返回主页
// 其他所有状态(包括注册页面)都返回到上一步
if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_check) {
print('👆 调用 Get.back() 返回主页');
Get.back();
print('👆 Get.back() 调用完成');
} else {
// 其他状态:返回到上一步(例如从注册页返回登录页)
print('👆 调用 controller.kr_back() 返回上一步');
controller.kr_back();
}
},
child: Padding(
padding: EdgeInsets.fromLTRB(0, 20.w, 0, 0),
@@ -1095,4 +1195,293 @@ class KRLoginView extends GetView<KRLoginController> {
),
);
}
/// 构建可悬停的文本按钮
Widget _buildHoverableTextButton({
required String text,
required VoidCallback onTap,
required ThemeData theme,
}) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: StatefulBuilder(
builder: (context, setState) {
bool isHovering = false;
return MouseRegion(
onEnter: (_) => setState(() => isHovering = true),
onExit: (_) => setState(() => isHovering = false),
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Text(
text,
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 13.sp,
color: isHovering ? const Color(0xFF1796FF) : const Color(0xFF666666),
fontWeight: FontWeight.w500,
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
);
},
),
);
}
/// ========== 注册页面相关组件 ==========
/// 构建注册页面的邮箱输入框
Widget _buildRegistrationEmailInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_account"),
),
SizedBox(width: 8.w),
Expanded(
child: TextField(
controller: controller.accountController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'login.enterEmail'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
Obx(() => Visibility(
visible: controller.kr_accountHasText.value,
child: GestureDetector(
onTap: () => controller.accountController.clear(),
child: Container(
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: _buildIcon("login_close"),
),
),
)),
],
),
);
}
/// 构建注册页面的验证码输入框(包含间距)
Widget _buildRegistrationCodeInputWithSpacing(ThemeData theme) {
// 从站点配置服务获取验证配置(站点配置不是响应式的,不需要 Obx)
final siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
// 如果不需要验证码,返回空容器
if (!needVerification) {
return SizedBox.shrink();
}
// 显示验证码输入框和底部间距
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildRegistrationCodeInput(theme),
SizedBox(height: 8.w),
],
);
}
/// 构建注册页面的验证码输入框(根据站点配置显示)
Widget _buildRegistrationCodeInput(ThemeData theme) {
// 从站点配置服务获取验证配置
final siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
// 如果不需要验证码,返回空容器
if (!needVerification) {
return SizedBox.shrink();
}
// 显示验证码输入框
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_code"),
),
SizedBox(width: 8.w),
Expanded(
child: TextField(
controller: controller.codeController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'login.enterCode'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
if (controller.kr_codeHasText.value)
GestureDetector(
onTap: () => controller.codeController.clear(),
child: Container(
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: _buildIcon("login_close"),
),
),
_buildSendCodeButton(theme),
],
),
);
}
/// 构建注册页面的密码输入框
Widget _buildRegistrationPasswordInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_psd"),
),
SizedBox(width: 8.w),
Expanded(
child: Obx(() => TextField(
controller: controller.psdController,
obscureText: controller.kr_obscureText.value,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: 'login.enterPassword'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
suffixIcon: controller.kr_psdHasText.value
? IconButton(
icon: Icon(
controller.kr_obscureText.value
? Icons.visibility_off
: Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: () {
controller.kr_obscureText.value = !controller.kr_obscureText.value;
},
)
: null,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
)),
),
],
),
);
}
/// 构建注册页面的确认密码输入框
Widget _buildRegistrationConfirmPasswordInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_psd"),
),
SizedBox(width: 8.w),
Expanded(
child: Obx(() => TextField(
controller: controller.agPsdController,
obscureText: controller.kr_obscureText.value,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: 'login.reenterPassword'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
suffixIcon: controller.kr_agPsdHasText.value
? IconButton(
icon: Icon(
controller.kr_obscureText.value
? Icons.visibility_off
: Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: () {
controller.kr_obscureText.value = !controller.kr_obscureText.value;
},
)
: null,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
)),
),
],
),
);
}
}
@@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../../../common/app_run_data.dart';
import '../../../common/app_config.dart';
import '../../../model/response/kr_already_subscribe.dart';
import '../../../model/response/kr_payment_methods.dart';
import '../../../routes/app_pages.dart';
@@ -109,15 +110,20 @@ class KRPurchaseMembershipController extends GetxController {
/// 初始化用户信息
Future<void> _iniUserInfo() async {
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
_kr_balance = userInfo.balance;
},
);
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 余额现在使用 AppConfig 中的固定值,等待新接口实现
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) {
// KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
// },
// (userInfo) async {
// _kr_balance = userInfo.balance;
// },
// );
// 使用 AppConfig 中的固定余额
_kr_balance = AppConfig.kr_userBalance;
}
/// 获取用户已订阅套餐
@@ -8,6 +8,7 @@ import '../controllers/kr_setting_controller.dart';
import '../../../themes/kr_theme_service.dart';
import '../../../localization/app_translations.dart';
import '../../../routes/app_pages.dart';
import '../../../common/app_run_data.dart';
class KRSettingView extends GetView<KRSettingController> {
const KRSettingView({Key? key}) : super(key: key);
@@ -235,12 +236,33 @@ class KRSettingView extends GetView<KRSettingController> {
onChanged: (value) => controller.kr_helpImprove.value = value,
),
_kr_buildDivider(),
_kr_buildActionTile(
context,
title: AppTranslations.kr_userInfo.myAccount,
trailing: AppTranslations.kr_setting.goToDelete,
onTap: controller.kr_deleteAccount,
),
Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
if (!isLoggedIn) {
// 未登录,不显示此项
return SizedBox.shrink();
} else if (isDeviceLogin) {
// 设备登录(游客模式),显示"点击这里登录/注册"
return _kr_buildActionTile(
context,
title: "登录/注册",
trailing: "",
onTap: () => Get.toNamed(Routes.MR_LOGIN),
);
} else {
// 正常登录,显示用户邮箱
final userEmail = appRunData.kr_account.value ?? AppTranslations.kr_userInfo.myAccount;
return _kr_buildActionTile(
context,
title: userEmail,
trailing: AppTranslations.kr_setting.goToDelete,
onTap: controller.kr_deleteAccount,
);
}
}),
_kr_buildDivider(),
// _kr_buildTitleTile(
// context,
@@ -407,7 +429,7 @@ class KRSettingView extends GetView<KRSettingController> {
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
trailing: Text(
@@ -241,13 +241,29 @@ class KRSplashController extends GetxController {
}
// 等待一小段时间确保所有初始化完成
await Future.delayed(const Duration(milliseconds: 200));
await Future.delayed(const Duration(milliseconds: 500));
// 验证登录状态是否已正确设置
final loginStatus = KRAppRunData.getInstance().kr_isLogin.value;
KRLogUtil.kr_i('启动完成,最终登录状态: $loginStatus', tag: 'SplashController');
final token = KRAppRunData.getInstance().kr_token;
final hasToken = token != null && token.isNotEmpty;
// 直接导航到主页
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('🎯 准备进入主页');
print('📊 最终登录状态: $loginStatus');
print('🎫 Token存在: $hasToken');
if (hasToken) {
print('🎫 Token前缀: ${token.substring(0, min(20, token.length))}...');
}
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
KRLogUtil.kr_i('🎯 准备进入主页', tag: 'SplashController');
KRLogUtil.kr_i('📊 最终登录状态: $loginStatus', tag: 'SplashController');
KRLogUtil.kr_i('🎫 Token存在: $hasToken', tag: 'SplashController');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
// 直接导航到主页(无论是否登录,主页会根据登录状态显示不同内容)
Get.offAllNamed(Routes.KR_MAIN);
} catch (e) {
// 后续步骤失败,显示错误信息
@@ -102,13 +102,15 @@ class KRStatisticsController extends GetxController {
return;
}
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(userInfo) {
// kr_homeController.kr_userId.value = userInfo.id.toString();
},
);
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 用户ID 现在从其他途径获取,此处注释掉
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) => KRCommonUtil.kr_showToast(error.msg),
// (userInfo) {
// // kr_homeController.kr_userId.value = userInfo.id.toString();
// },
// );
// 获取本周的开始和结束时间戳
final DateTime now = DateTime.now();
@@ -199,15 +199,21 @@ class KRUserInfoController extends GetxController with KRAppBarOpacityMixin {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
return;
}
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
kr_balance.value = userInfo.balance.toDouble() / 100;
},
);
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 余额现在使用 AppConfig 中的固定值,等待新接口实现
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) {
// KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
// },
// (userInfo) async {
// kr_balance.value = userInfo.balance.toDouble() / 100;
// },
// );
// 使用 AppConfig 中的固定余额
kr_balance.value = AppConfig.kr_userBalance.toDouble() / 100;
}
/// 处理用户退出登录
@@ -99,64 +99,115 @@ class KRUserInfoView extends GetView<KRUserInfoController> {
// 构建绑定提示
Widget _kr_buildBindingTip(BuildContext context) {
return Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
return Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
// 判断显示文字和颜色
String displayText;
Color displayColor;
IconData displayIcon;
bool shouldShowLoginPrompt = false;
if (!isLoggedIn) {
// 未登录
displayText = AppTranslations.kr_userInfo.bindingTip;
displayColor = Theme.of(context).colorScheme.error;
displayIcon = Icons.info_outline;
shouldShowLoginPrompt = false;
} else if (isDeviceLogin) {
// 设备登录(游客模式)
displayText = "登录/注册";
displayColor = const Color(0xFF1797FF); // 使用蓝色提示可以点击
displayIcon = Icons.touch_app;
shouldShowLoginPrompt = true;
} else {
// 正常登录
displayText = "${AppTranslations.kr_userInfo.myAccount} ${appRunData.kr_account.value}";
displayColor = Theme.of(context).textTheme.bodyMedium?.color ?? Colors.black;
displayIcon = Icons.info;
shouldShowLoginPrompt = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: shouldShowLoginPrompt
? () {
// 跳转到登录页面
Get.toNamed(Routes.MR_LOGIN);
}
: null,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
decoration: shouldShowLoginPrompt
? BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.05),
borderRadius: BorderRadius.circular(8.w),
)
: null,
child: Row(
children: [
Icon(
KRAppRunData.getInstance().kr_isLogin.value
? Icons.info
: Icons.info_outline,
color: !KRAppRunData.getInstance().kr_isLogin.value
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodyMedium?.color,
displayIcon,
color: displayColor,
size: 16.w,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
displayText,
style: KrAppTextStyle(
color: displayColor,
fontSize: 12,
fontWeight: shouldShowLoginPrompt ? FontWeight.w600 : FontWeight.normal,
),
),
),
if (shouldShowLoginPrompt)
Icon(
Icons.arrow_forward_ios,
size: 14.w,
color: displayColor,
),
],
),
),
),
// 余额信息或游客ID
Visibility(
visible: KRAppRunData.getInstance().kr_isLogin.value &&
AppConfig.getInstance().kr_is_daytime,
child: Padding(
padding: EdgeInsets.only(left: 16.w, bottom: 16.w),
child: Row(
children: [
Icon(
appRunData.isDeviceLogin()
? Icons.person_outline
: Icons.account_balance_wallet_outlined,
color: Theme.of(context).textTheme.bodyMedium?.color,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
KRAppRunData.getInstance().kr_isLogin.value
? "${AppTranslations.kr_userInfo.myAccount} ${KRAppRunData().kr_account}"
: AppTranslations.kr_userInfo.bindingTip,
appRunData.isDeviceLogin()
? "游客ID${(appRunData.kr_userId.value ?? 0) + 10000}"
: "${AppTranslations.kr_userInfo.balance} ${controller.kr_balance.value.toString()}",
style: KrAppTextStyle(
color: !KRAppRunData.getInstance().kr_isLogin.value
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
// 余额信息(写死预览)
Visibility(
visible: KRAppRunData.getInstance().kr_isLogin.value &&
AppConfig.getInstance().kr_is_daytime,
child: Padding(
padding: EdgeInsets.only(left: 16.w, bottom: 16.w),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_outlined,
color: Theme.of(context).textTheme.bodyMedium?.color,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
"${AppTranslations.kr_userInfo.balance} ${controller.kr_balance.value.toString()}",
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
),
],
));
),
],
);
});
}
// 构建订阅卡片
@@ -574,49 +625,67 @@ class KRUserInfoView extends GetView<KRUserInfoController> {
// 构建快捷键区域
Widget _kr_buildShortcutSection(BuildContext context) {
return Container(
margin: EdgeInsets.fromLTRB(16.w, 24.w, 16.w, 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_userInfo.shortcuts,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
return Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
// 只有正常登录用户(非游客)才显示设备管理
final showDeviceManagement = isLoggedIn && !isDeviceLogin;
return Container(
margin: EdgeInsets.fromLTRB(16.w, 24.w, 16.w, 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_userInfo.shortcuts,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
SizedBox(height: 12.w),
Column(
children: [
_kr_buildShortcutContainer(
icon: "my_ads",
title: AppTranslations.kr_userInfo.adBlock,
value: controller.kr_isAdBlockEnabled,
onChanged: controller.kr_toggleAdBlock,
context: context,
),
_kr_buildShortcutContainer(
icon: "my_dns",
title: AppTranslations.kr_userInfo.ndsUnlock,
value: controller.kr_isNDSUnlockEnabled,
onChanged: controller.kr_toggleNDSUnlock,
context: context,
),
_kr_buildShortcutContainer(
icon: "my_cn_us",
title: AppTranslations.kr_userInfo.contactUs,
onTap: () {
Get.toNamed(Routes.KR_CRISP);
},
context: context,
),
],
),
],
),
);
SizedBox(height: 12.w),
Column(
children: [
_kr_buildShortcutContainer(
icon: "my_ads",
title: AppTranslations.kr_userInfo.adBlock,
value: controller.kr_isAdBlockEnabled,
onChanged: controller.kr_toggleAdBlock,
context: context,
),
_kr_buildShortcutContainer(
icon: "my_dns",
title: AppTranslations.kr_userInfo.ndsUnlock,
value: controller.kr_isNDSUnlockEnabled,
onChanged: controller.kr_toggleNDSUnlock,
context: context,
),
if (showDeviceManagement)
_kr_buildShortcutContainer(
icon: "my_dns",
title: "设备管理",
onTap: () {
Get.toNamed(Routes.KR_DEVICE_MANAGEMENT);
},
context: context,
),
_kr_buildShortcutContainer(
icon: "my_cn_us",
title: AppTranslations.kr_userInfo.contactUs,
onTap: () {
Get.toNamed(Routes.KR_CRISP);
},
context: context,
),
],
),
],
),
);
});
}
// 构建快捷键容器