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

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
+15
View File
@@ -1071,6 +1071,21 @@ class AppConfig {
/// 网站ID
String kr_website_id = "";
/// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// 用户信息固定值(临时)
/// ⚠️ 注意:新版本后端已废弃 /v1/app/user/info 接口
/// 等待新接口实现后,这些值应该从新接口动态获取
/// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// 用户余额(单位:分)
/// 临时固定值:0,表示0.00元
static const int kr_userBalance = 0;
/// 用户邀请码
/// 临时固定值:空字符串,待新接口实现
static const String kr_userReferCode = "";
/// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// 是否为白天模式
bool kr_is_daytime = true;
+90 -39
View File
@@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../services/api_service/kr_api.user.dart';
import '../services/kr_announcement_service.dart';
import '../utils/kr_event_bus.dart';
class KRAppRunData {
@@ -23,11 +24,11 @@ class KRAppRunData {
/// 登录token
String? kr_token;
/// 用户账号
String? kr_account;
/// 用户账号(使用响应式变量以便 UI 能监听变化)
final Rx<String?> kr_account = Rx<String?>(null);
/// 用户ID
String? kr_userId;
/// 用户ID(使用响应式变量以便 UI 能监听变化)
final Rx<int?> kr_userId = Rx<int?>(null);
/// 登录类型
KRLoginType? kr_loginType;
@@ -46,17 +47,77 @@ class KRAppRunData {
return _instance;
}
/// 判断是否是设备登录(游客模式)
bool isDeviceLogin() {
// 设备登录的账号格式为 "device_设备ID"
return kr_account.value != null && kr_account.value!.startsWith('device_');
}
/// 从JWT token中解析userId
int? _kr_parseUserIdFromToken(String token) {
try {
// JWT格式: header.payload.signature
final parts = token.split('.');
if (parts.length != 3) {
KRLogUtil.kr_e('JWT token格式错误', tag: 'AppRunData');
return null;
}
// 解码payload部分(base64
String payload = parts[1];
// 手动添加必要的padding(base64要求长度是4的倍数)
switch (payload.length % 4) {
case 0:
break; // 不需要padding
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
default:
KRLogUtil.kr_e('JWT payload长度无效', tag: 'AppRunData');
return null;
}
final decodedBytes = base64.decode(payload);
final decodedString = utf8.decode(decodedBytes);
// 解析JSON
final Map<String, dynamic> payloadMap = jsonDecode(decodedString);
// 获取UserId
if (payloadMap.containsKey('UserId')) {
final userId = payloadMap['UserId'];
KRLogUtil.kr_i('从JWT解析出userId: $userId', tag: 'AppRunData');
return userId is int ? userId : int.tryParse(userId.toString());
}
return null;
} catch (e) {
KRLogUtil.kr_e('解析JWT token失败: $e', tag: 'AppRunData');
return null;
}
}
/// 保存用户信息
Future<void> kr_saveUserInfo(
String token, String account, KRLoginType loginType, String? areaCode) async {
String token,
String account,
KRLoginType loginType,
String? areaCode) async {
KRLogUtil.kr_i('开始保存用户信息', tag: 'AppRunData');
try {
// 更新内存中的数据
kr_token = token;
kr_account = account;
kr_account.value = account;
kr_loginType = loginType;
kr_areaCode = areaCode;
// 从JWT token中解析userId
kr_userId.value = _kr_parseUserIdFromToken(token);
KRLogUtil.kr_i('从JWT解析userId: ${kr_userId.value}', tag: 'AppRunData');
final Map<String, dynamic> userInfo = {
'token': token,
@@ -81,15 +142,13 @@ class KRAppRunData {
}
KRLogUtil.kr_i('用户信息保存成功,设置登录状态为true', tag: 'AppRunData');
// 只有在保存成功后才设置登录状态
kr_isLogin.value = true;
// 异步获取用户信息并建立 Socket 连接,不等待结果
_iniUserInfo().catchError((error) {
KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData');
// 即使获取用户信息失败,也保持登录状态
});
// 设备登录模式不再调用用户信息接口
// Socket 连接将在需要时建立
KRLogUtil.kr_i('用户信息已保存,跳过用户信息接口调用', tag: 'AppRunData');
} catch (e) {
KRLogUtil.kr_e('保存用户信息失败: $e', tag: 'AppRunData');
@@ -103,20 +162,23 @@ class KRAppRunData {
Future<void> kr_loginOut() async {
// 先将登录状态设置为 false,防止重连
kr_isLogin.value = false;
// 断开 Socket 连接
await _kr_disconnectSocket();
// 清理用户信息
kr_token = null;
kr_account = null;
kr_userId = null;
kr_account.value = null;
kr_userId.value = null;
kr_loginType = null;
kr_areaCode = null;
// 删除存储的用户信息
await KRSecureStorage().kr_deleteData(key: _keyUserInfo);
// 重置公告显示状态
KRAnnouncementService().kr_reset();
// 重置主页面
Get.find<KRMainController>().kr_setPage(0);
}
@@ -135,7 +197,7 @@ class KRAppRunData {
try {
final Map<String, dynamic> userInfo = jsonDecode(userInfoString);
kr_token = userInfo['token'];
kr_account = userInfo['account'];
kr_account.value = userInfo['account'];
final loginTypeValue = userInfo['loginType'];
kr_loginType = KRLoginType.values.firstWhere(
(e) => e.value == loginTypeValue,
@@ -143,18 +205,21 @@ class KRAppRunData {
);
kr_areaCode = userInfo['areaCode'] ?? "";
KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=$kr_account', tag: 'AppRunData');
// 从token中解析userId
if (kr_token != null && kr_token!.isNotEmpty) {
kr_userId.value = _kr_parseUserIdFromToken(kr_token!);
}
KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=${kr_account.value}', tag: 'AppRunData');
// 验证token有效性
if (kr_token != null && kr_token!.isNotEmpty) {
KRLogUtil.kr_i('设置登录状态为true', tag: 'AppRunData');
kr_isLogin.value = true;
// 异步获取用户信息,但不等待结果
_iniUserInfo().catchError((error) {
KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData');
// 如果获取用户信息失败,不重置登录状态,让用户重试
});
// 设备登录模式不需要调用用户信息接口
// 用户ID将从订阅信息或其他途径获取
KRLogUtil.kr_i('已登录,跳过用户信息接口调用', tag: 'AppRunData');
} else {
KRLogUtil.kr_w('Token为空,设置为未登录状态', tag: 'AppRunData');
kr_isLogin.value = false;
@@ -175,20 +240,6 @@ class KRAppRunData {
KRLogUtil.kr_i('用户信息初始化完成,登录状态: ${kr_isLogin.value}', tag: 'AppRunData');
}
/// 初始化用户信息并建立 Socket 连接
Future<void> _iniUserInfo() async {
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
kr_userId = userInfo.id.toString();
_kr_connectSocket(kr_userId!);
},
);
}
/// 建立 Socket 连接
Future<void> _kr_connectSocket(String userId) async {
// 如果已存在连接,先断开
@@ -123,6 +123,9 @@ class AppTranslationsLogin {
/// 输入邮箱或手机号提示
String get enterEmailOrPhone => 'login.enterEmailOrPhone'.tr;
/// 输入邮箱提示
String get enterEmail => 'login.enterEmail'.tr;
/// 输入验证码提示
String get enterCode => 'login.enterCode'.tr;
+109 -1
View File
@@ -45,7 +45,24 @@ class KROutboundItem {
city = nodeListItem.city; // 设置城市
country = nodeListItem.country; // 设置国家
final json = jsonDecode(nodeListItem.config);
// 安全解析 config 字段
// 新API格式:config为空,直接使用节点字段构建配置
// 旧API格式:config包含JSON配置
if (nodeListItem.config.isEmpty) {
print('️ 节点 ${nodeListItem.name} 使用直接字段构建配置');
_buildConfigFromFields(nodeListItem);
return;
}
late Map<String, dynamic> json;
try {
json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
} catch (e) {
print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段');
print('📄 Config 内容: ${nodeListItem.config}');
_buildConfigFromFields(nodeListItem);
return;
}
switch (nodeListItem.protocol) {
case "vless":
final securityConfig =
@@ -208,4 +225,95 @@ class KROutboundItem {
return {};
}
}
/// 直接从节点字段构建配置(新API格式)
void _buildConfigFromFields(KrNodeListItem nodeListItem) {
switch (nodeListItem.protocol) {
case "shadowsocks":
config = {
"type": "shadowsocks",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"method": "chacha20-ietf-poly1305", // 默认加密方法
"password": nodeListItem.uuid
};
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
break;
case "vless":
config = {
"type": "vless",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"uuid": nodeListItem.uuid,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
}
};
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
break;
case "vmess":
config = {
"type": "vmess",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"uuid": nodeListItem.uuid,
"alter_id": 0,
"security": "auto",
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
break;
case "trojan":
config = {
"type": "trojan",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"password": nodeListItem.uuid,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}');
break;
case "hysteria2":
config = {
"type": "hysteria2",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"password": nodeListItem.uuid,
"up_mbps": 100,
"down_mbps": 100,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"alpn": ["h3"]
}
};
print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}');
break;
default:
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
config = {};
}
}
}
@@ -45,8 +45,15 @@ class KrOutboundsList {
}
final KROutboundItem item = KROutboundItem(element);
// 检查节点配置是否有效(必须包含 type 字段)
if (item.config.isEmpty || !item.config.containsKey('type')) {
print('⚠️ 跳过无效节点: ${element.name},配置为空或缺少 type 字段');
continue; // 跳过无效节点
}
allList.add(item);
// 根据标签分组出站项
for (var tag in element.tags) {
tagGroups.putIfAbsent(tag, () => []);
+33 -7
View File
@@ -5,23 +5,39 @@ class KRNodeList {
final String subscribeId;
final String startTime;
final String expireTime;
final bool isTryOut; // 是否是试用订阅
const KRNodeList({
required this.list,
this.subscribeId = "0",
this.startTime = "",
this.expireTime = "",
this.isTryOut = false,
});
factory KRNodeList.fromJson(Map<String, dynamic> json) {
try {
final List<dynamic>? jsonList= json['list'] as List<dynamic>?;
// 新的 API 返回格式: {"list": [{"id": 24, "is_try_out": true, "nodes": [...]}]}
final List<dynamic>? listData = json['list'] as List<dynamic>?;
if (listData == null || listData.isEmpty) {
KRLogUtil.kr_w('节点列表为空', tag: 'NodeList');
return const KRNodeList(list: []);
}
// 获取第一个订阅对象
final subscribeData = listData[0] as Map<String, dynamic>;
final bool isTryOut = subscribeData['is_try_out'] as bool? ?? false;
final List<dynamic>? nodesData = subscribeData['nodes'] as List<dynamic>?;
KRLogUtil.kr_i('节点列表解析: is_try_out=$isTryOut, 节点数=${nodesData?.length ?? 0}', tag: 'NodeList');
return KRNodeList(
list: jsonList?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: json['id']?.toString() ?? "0",
startTime: json['start_time']?.toString() ?? "",
expireTime: json['expire_time']?.toString() ?? "",
list: nodesData?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: subscribeData['id']?.toString() ?? "0",
startTime: subscribeData['start_time']?.toString() ?? "",
expireTime: subscribeData['expire_time']?.toString() ?? "",
isTryOut: isTryOut,
);
} catch (err) {
KRLogUtil.kr_e('KRNodeList解析错误: $err', tag: 'NodeList');
@@ -38,6 +54,7 @@ class KrNodeListItem {
final String relayMode;
final String relayNode;
final String serverAddr;
final int port; // 新增:端口字段
final int speedLimit;
final List<String> tags;
final int traffic;
@@ -63,6 +80,7 @@ class KrNodeListItem {
this.relayMode = '',
this.relayNode = '',
required this.serverAddr,
this.port = 0, // 默认值
required this.speedLimit,
required this.tags,
required this.traffic,
@@ -83,6 +101,12 @@ class KrNodeListItem {
factory KrNodeListItem.fromJson(Map<String, dynamic> json) {
try {
// 支持新旧两种API格式
// 新格式: address, port
// 旧格式: server_addr, config 中包含 port
final serverAddr = json['address']?.toString() ?? json['server_addr']?.toString() ?? '';
final port = _parseIntSafely(json['port']);
return KrNodeListItem(
id: _parseIntSafely(json['id']),
name: json['name']?.toString() ?? '',
@@ -90,7 +114,8 @@ class KrNodeListItem {
protocol: json['protocol']?.toString() ?? '',
relayMode: json['relay_mode']?.toString() ?? '',
relayNode: json['relay_node']?.toString() ?? '',
serverAddr: json['server_addr']?.toString() ?? '',
serverAddr: serverAddr,
port: port,
speedLimit: _parseIntSafely(json['speed_limit']),
tags: _parseStringList(json['tags']),
traffic: _parseIntSafely(json['traffic']),
@@ -116,6 +141,7 @@ class KrNodeListItem {
uuid: '',
protocol: '',
serverAddr: '',
port: 0,
speedLimit: 0,
tags: [],
traffic: 0,
@@ -10,6 +10,7 @@ class KRUserAvailableSubscribeItem {
final String startTime;
final String expireTime;
final List<dynamic> list;
final bool isTryOut; // 试用标志:true=试用,false=付费
const KRUserAvailableSubscribeItem({
this.id = 0,
@@ -21,19 +22,35 @@ class KRUserAvailableSubscribeItem {
this.startTime = '',
this.expireTime = '',
this.list = const [],
this.isTryOut = false,
});
factory KRUserAvailableSubscribeItem.fromJson(Map<String, dynamic> json) {
// 从 subscribe 对象中获取订阅信息
final subscribe = json['subscribe'] as Map<String, dynamic>?;
// 时间字段可能是 int (毫秒时间戳) 或 String (ISO 8601)
String convertTime(dynamic value) {
if (value == null) return '';
if (value is String) return value;
if (value is int) {
// 将毫秒时间戳转换为 ISO 8601 字符串
return DateTime.fromMillisecondsSinceEpoch(value).toIso8601String();
}
return value.toString();
}
return KRUserAvailableSubscribeItem(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? '',
deviceLimit: json['device_limit'] as int? ?? 0,
name: subscribe?['name'] as String? ?? '',
deviceLimit: subscribe?['device_limit'] as int? ?? 0,
download: json['download'] as int? ?? 0,
upload: json['upload'] as int? ?? 0,
traffic: json['traffic'] as int? ?? 0,
startTime: json['start_time'] as String? ?? '',
expireTime: json['expire_time'] as String? ?? '',
startTime: convertTime(json['start_time']),
expireTime: convertTime(json['expire_time']),
list: (json['list'] as List<dynamic>?) ?? const [],
isTryOut: subscribe?['is_try_out'] as bool? ?? false,
);
}
@@ -48,6 +65,7 @@ class KRUserAvailableSubscribeItem {
'start_time': startTime,
'expire_time': expireTime,
'list': list,
'is_try_out': isTryOut,
};
}
}
@@ -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,
),
],
),
],
),
);
});
}
// 构建快捷键容器
+4
View File
@@ -34,6 +34,10 @@ class BaseResponse<T> {
final decrypted = KRAesUtil.decryptData(cipherText, nonce, AppConfig.kr_encryptionKey);
body = jsonDecode(decrypted);
KRLogUtil.kr_i('✅ 解密成功', tag: 'BaseResponse');
// 打印完整的解密后数据,方便调试
final bodyStr = jsonEncode(body);
KRLogUtil.kr_i('📦 解密后数据(完整): $bodyStr', tag: 'BaseResponse');
} catch (e) {
KRLogUtil.kr_e('❌ 解密失败: $e,使用原始数据', tag: 'BaseResponse');
body = dataMap;
+10 -1
View File
@@ -25,7 +25,7 @@ import '../utils/kr_log_util.dart';
// import 'package:video/app/utils/log_util.dart';
/// 定义请求方法的枚举
enum HttpMethod { GET, POST, DELETE }
enum HttpMethod { GET, POST, DELETE, PUT }
/// 封装请求
class HttpUtil {
@@ -167,6 +167,15 @@ class HttpUtil {
headers: headers, // 添加请求头
),
);
} else if (method == HttpMethod.PUT) {
responseTemp = await _dio.put<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, // 添加请求头
),
);
} else {
responseTemp = await _dio.post<Map<String, dynamic>>(
path,
+7
View File
@@ -32,6 +32,8 @@ import '../modules/kr_order_status/bindings/kr_order_status_binding.dart';
import '../modules/kr_order_status/views/kr_order_status_view.dart';
import '../modules/kr_splash/bindings/kr_splash_binding.dart';
import '../modules/kr_splash/views/kr_splash_view.dart';
import '../modules/kr_device_management/bindings/kr_device_management_binding.dart';
import '../modules/kr_device_management/views/kr_device_management_view.dart';
part 'app_routes.dart';
@@ -121,5 +123,10 @@ class AppPages {
page: () => const KRCrispView(),
binding: KRCrispBinding(),
),
GetPage(
name: _Paths.KR_DEVICE_MANAGEMENT,
page: () => const KRDeviceManagementView(),
binding: KRDeviceManagementBinding(),
),
];
}
+2
View File
@@ -20,6 +20,7 @@ abstract class Routes {
static const KR_WEBVIEW = _Paths.KR_WEBVIEW;
static const KR_ORDER_STATUS = '/kr-order-status';
static const KR_CRISP = _Paths.KR_CRISP;
static const KR_DEVICE_MANAGEMENT = _Paths.KR_DEVICE_MANAGEMENT;
}
abstract class _Paths {
@@ -40,4 +41,5 @@ abstract class _Paths {
static const KR_DELETE_ACCOUNT = '/kr-delete-account';
static const KR_WEBVIEW = '/kr_webview';
static const KR_CRISP = '/kr-crisp';
static const KR_DEVICE_MANAGEMENT = '/kr-device-management';
}
+20 -21
View File
@@ -3,21 +3,17 @@ abstract class Api {
/// 游客登录查看是否已经注册
static const String kr_isRegister = "/v1/app/auth/check";
/// 注册1024
static const String kr_register = "/v1/app/auth/register";
/// 注册
static const String kr_register = "/v1/auth/register";
/// 验证验证码
static const String kr_checkVerificationCode =
"/v1/common/check_verification_code";
static const String kr_checkVerificationCode = "/v1/auth/check-code";
/// 发送手机验证码
static const String kr_sendPhoneCode = "/v1/common/send_sms_code";
/// 发送邮箱验证码
static const String kr_sendEmailCode = "/v1/common/send_code";
/// 发送验证码(统一接口,支持邮箱和手机)
static const String kr_sendCode = "/v1/auth/send-code";
/// 登录接口
static const String kr_login = "/v1/app/auth/login";
static const String kr_login = "/v1/auth/login";
/// 设备登录(游客登录)
/// 参考 OmnOem 项目 ppanel.json 配置
@@ -29,8 +25,8 @@ abstract class Api {
/// 忘记密码-设置新密码
static const String kr_setNewPsdByForgetPsd = "/v1/app/auth/reset_password";
/// 节点信息
static const String kr_nodeList = "/v1/app/node/list";
/// 节点信息(包含试用/付费标志)
static const String kr_nodeList = "/v1/public/subscribe/node/list";
/// 获取用户订阅流量日志
static const String kr_nodeGroupList = "/v1/app/node/rule_group_list";
@@ -48,15 +44,15 @@ abstract class Api {
static const String kr_checkout = "/v1/app/order/checkout";
/// 获取可购买套餐
static const String kr_getPackageList = "/v1/app/subscribe/list";
static const String kr_getPackageList = "/v1/public/subscribe/list";
/// 获取用户已订阅套餐
/// 获取用户已订阅套餐(用于判断是否购买过)
static const String kr_getAlreadySubscribe =
"/v1/app/subscribe/user/already_subscribe";
"/v1/public/user/subscribe";
/// 获取用户可用订阅
/// 获取用户可用订阅(与已订阅接口相同,OmnOem 项目中没有区分)
static const String kr_userAvailableSubscribe =
"/v1/app/subscribe/user/available_subscribe";
"/v1/public/user/subscribe";
/// 续费
static const String kr_renewal = "/v1/app/order/renewal";
@@ -66,7 +62,7 @@ abstract class Api {
static const String kr_orderDetail = "/v1/app/order/detail";
/// 获取消息列表
static const String kr_getMessageList = "/v1/app/announcement/list";
static const String kr_getMessageList = "/v1/public/announcement/list";
/// 获取邀请数据
// static const String kr_getInviteData = "/v1/public/invite/code";
@@ -74,9 +70,6 @@ abstract class Api {
/// 配置信息
static const String kr_config = "/v1/app/auth/config";
/// 获取用户信息
static const String kr_getUserInfo = "/v1/app/user/info";
/// 获取用户在线时长统计
static const String kr_getUserOnlineTimeStatistics =
"/v1/app/user/online_time/statistics";
@@ -96,4 +89,10 @@ abstract class Api {
/// 重置订阅周期
static const String kr_resetSubscribePeriod =
"/v1/app/subscribe/reset/period";
/// 获取用户设备列表
static const String kr_getUserDevices = "/v1/public/user/devices";
/// 解绑用户设备
static const String kr_unbindUserDevice = "/v1/public/user/unbind_device";
}
+76 -15
View File
@@ -83,21 +83,22 @@ class KRUserApi {
return right(baseResponse.model);
}
Future<Either<HttpError, KRUserInfo>> kr_getUserInfo() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRUserInfo> baseResponse =
await HttpUtil.getInstance().request<KRUserInfo>(
Api.kr_getUserInfo,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
// ⚠️ 已废弃:新版本后端不再提供此接口
// Future<Either<HttpError, KRUserInfo>> kr_getUserInfo() async {
// final Map<String, dynamic> data = <String, dynamic>{};
// BaseResponse<KRUserInfo> baseResponse =
// await HttpUtil.getInstance().request<KRUserInfo>(
// Api.kr_getUserInfo,
// data,
// method: HttpMethod.GET,
// isShowLoading: false,
// );
// if (!baseResponse.isSuccess) {
// return left(
// HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
// }
// return right(baseResponse.model);
// }
Future<Either<HttpError, KRAffiliateCount>> kr_getAffiliateCount() async {
final Map<String, dynamic> data = <String, dynamic>{};
@@ -132,4 +133,64 @@ class KRUserApi {
return right(baseResponse.model);
}
/// 获取用户设备列表
Future<Either<HttpError, List<Map<String, dynamic>>>> kr_getUserDevices() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_getUserDevices,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
// 返回设备列表数据
try {
// 响应格式: { data: { list: [...], total: N } }
final responseData = baseResponse.model;
final List<Map<String, dynamic>> devices =
(responseData['list'] as List)
.map((item) => item as Map<String, dynamic>)
.toList();
return right(devices);
} catch (e) {
KRLogUtil.kr_e('解析设备列表失败: $e', tag: 'KRUserApi');
return left(HttpError(msg: '数据解析失败', code: -1));
}
}
/// 解绑用户设备
Future<Either<HttpError, void>> kr_unbindUserDevice(String deviceId) async {
final Map<String, dynamic> data = <String, dynamic>{};
// 将字符串 ID 转换为整数
try {
data['id'] = int.parse(deviceId);
} catch (e) {
KRLogUtil.kr_e('设备ID格式错误: $deviceId', tag: 'KRUserApi');
return left(HttpError(msg: '设备ID格式错误', code: -1));
}
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_unbindUserDevice,
data,
method: HttpMethod.PUT,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(null);
}
}
+47 -100
View File
@@ -23,22 +23,14 @@ import '../../common/app_config.dart';
import 'package:dio/dio.dart' as dio;
class KRAuthApi {
/// 是否开启了审核开关
Future<Either<HttpError, bool>> kr_isRegister(
KRLoginType tpye, String account, String? areaCode) async {
/// 检查账号是否已注册(仅支持邮箱)
Future<Either<HttpError, bool>> kr_isRegister(String email) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
data['email'] = email;
data["user_agent"] = _kr_getUserAgent();
data["os"] = _kr_getUserAgent();
if (areaCode != null) {
data['area_code'] = areaCode.toString();
}
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_isRegister, data,
@@ -52,29 +44,26 @@ class KRAuthApi {
return right(baseResponse.model.kr_isRegister);
}
/// 注册
/// 注册(仅支持邮箱+密码,验证码和邀请码可选)
Future<Either<HttpError, String>> kr_register(
KRLoginType tpye,
String account,
String? areaCode,
String? code,
String? password,
{String? inviteCode}) async {
String email,
String password,
{String? code,
String? inviteCode}) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
data['email'] = email;
data['password'] = password;
data["code"] = code;
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
data["os"] = _kr_getUserAgent();
// 验证码是可选的,只有在提供时才发送
if (code != null && code.isNotEmpty) {
data["code"] = code;
}
// 邀请码是可选的
if (inviteCode != null && inviteCode.isNotEmpty) {
data["invite"] = inviteCode;
}
data["user_agent"] = _kr_getUserAgent();
if (tpye == KRLoginType.kr_telephone) {
data['area_code'] = areaCode;
}
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_register, data,
@@ -87,20 +76,14 @@ class KRAuthApi {
return right(baseResponse.model.kr_token.toString());
}
/// 验证验证码
Future<Either<HttpError, bool>> kr_checkVerificationCode( KRLoginType tpye,
String account, String? areaCode, String code, int type) async {
/// 验证验证码(仅支持邮箱)
Future<Either<HttpError, bool>> kr_checkVerificationCode(
String email, String code, int type) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
if(tpye == KRLoginType.kr_telephone){
data['account'] = areaCode.toString() + account;
}else{
data['account'] = account;
}
data['email'] = email;
data['code'] = code;
data['type'] = type;
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_checkVerificationCode, data,
method: HttpMethod.POST, isShowLoading: true);
@@ -108,37 +91,23 @@ class KRAuthApi {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
if(baseResponse.model.kr_isRegister){
if (baseResponse.model.kr_isRegister) {
return right(true);
}else{
} else {
return left(HttpError(msg: "error.70001".tr, code: 70001));
}
}
/// 登
Future<Either<HttpError, String>> kr_login(KRLoginType tpye, bool isPsd,
String account, String? areaCode, String? code, String? password) async {
/// 登录(仅支持邮箱+密码)
Future<Either<HttpError, String>> kr_login(
String email, String password) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
data['email'] = email;
data['password'] = password;
final deviceId = await KRDeviceUtil().kr_getDeviceId();
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
data["user_agent"] = _kr_getUserAgent();
data["os"] = _kr_getUserAgent();
if (tpye == KRLoginType.kr_telephone) {
data['area_code'] = areaCode;
}
if (isPsd) {
data['password'] = password;
} else {
data["code"] = code;
}
data["identifier"] = deviceId;
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_login, data,
@@ -151,45 +120,30 @@ class KRAuthApi {
return right(baseResponse.model.kr_token.toString());
}
/// 发送验证码 type 1 注册 其他 2
Future<Either<HttpError, bool>> kr_sendCode(
KRLoginType tpye, String account, String? areaCode, int type) async {
/// 发送验证码(仅支持邮箱)
/// type: 1=登录, 2=注册, 3=重置密码
Future<Either<HttpError, bool>> kr_sendCode(String email, int type) async {
final Map<String, dynamic> data = <String, dynamic>{};
if (tpye == KRLoginType.kr_email) {
data['email'] = account;
} else {
data['telephone'] = account;
data['telephone_area_code'] = areaCode.toString();
}
data['email'] = email;
data['type'] = type;
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(
tpye == KRLoginType.kr_email
? Api.kr_sendEmailCode
: Api.kr_sendPhoneCode,
data,
method: HttpMethod.POST,
isShowLoading: true);
.request<dynamic>(Api.kr_sendCode, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
// KRCommonUtil.kr_showToast(baseResponse.model.toString());
// KRIsRegister model = (baseResponse..model) as KRIsRegister;
return right(true);
}
/// 删除账号
Future<Either<HttpError, String>> kr_deleteAccount(KRLoginType tpye,
String code) async {
Future<Either<HttpError, String>> kr_deleteAccount(String code) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['code'] = code;
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(Api.kr_deleteAccount, data,
method: HttpMethod.DELETE, isShowLoading: true);
if (!baseResponse.isSuccess) {
@@ -198,23 +152,16 @@ class KRAuthApi {
}
return right("");
}
/// 忘记密码-设置新密码
Future<Either<HttpError, String>> kr_setNewPsdByForgetPsd(KRLoginType tpye,
String account, String? areaCode, String? code, String? password) async {
/// 忘记密码-设置新密码(仅支持邮箱)
Future<Either<HttpError, String>> kr_setNewPsdByForgetPsd(
String email, String code, String password) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
data['email'] = email;
data['password'] = password;
data["code"] = code;
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
data["user_agent"] = _kr_getUserAgent();
data["os"] = _kr_getUserAgent();
if (tpye == KRLoginType.kr_telephone) {
data['area_code'] = areaCode;
}
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_setNewPsdByForgetPsd, data,
@@ -21,6 +21,11 @@ class KRAnnouncementService {
KRAnnouncementService._internal();
// 重置公告显示状态(用于退出登录时)
void kr_reset() {
_kr_hasShownAnnouncement = false;
}
// 检查是否需要显示公告弹窗
Future<void> kr_checkAnnouncement() async {
if (_kr_hasShownAnnouncement) {
+118 -32
View File
@@ -72,6 +72,9 @@ class KRSubscribeService {
/// 是否处于试用状态
final RxBool kr_isTrial = false.obs;
/// 当前节点列表是否包含试用节点
final RxBool kr_hasTrialNodes = false.obs;
/// 订阅记录
final RxList<KRAlreadySubscribe> kr_alreadySubscribe =
<KRAlreadySubscribe>[].obs;
@@ -247,9 +250,13 @@ class KRSubscribeService {
result.fold((error) {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
}, (nodes) {
// 处理节点列表
// 记录当前节点列表是否包含试用节点
kr_hasTrialNodes.value = nodes.isTryOut;
KRLogUtil.kr_i('切换订阅 - 节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService');
// 处理节点列表(不使用分组)
final listModel = KrOutboundsList();
listModel.processOutboundItems(nodes.list, kr_nodeGroups);
listModel.processOutboundItems(nodes.list, []);
// 更新UI数据
groupOutboundList.value = listModel.groupOutboundList;
@@ -277,24 +284,45 @@ class KRSubscribeService {
_kr_trialTimer?.cancel();
_kr_subscriptionTimer?.cancel();
// 检查试用状态
final bool kr_isSubscribed = kr_currentSubscribe.value != null &&
kr_alreadySubscribe.any((subscribe) =>
kr_currentSubscribe.value?.id == subscribe.userSubscribeId);
if (kr_currentSubscribe.value == null) {
kr_isTrial.value = false;
return;
}
KRLogUtil.kr_i('当前订阅状态: ${kr_isSubscribed ? "已订阅" : "未订阅"}',
tag: 'SubscribeService');
KRLogUtil.kr_i('当前订阅ID: ${kr_currentSubscribe.value?.id}',
tag: 'SubscribeService');
KRLogUtil.kr_i(
'已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}',
tag: 'SubscribeService');
// 优先使用 API 返回的 isTryOut 字段判断试用状态
final currentSubscribe = kr_currentSubscribe.value!;
// 设置试用状态
kr_isTrial.value = kr_currentSubscribe.value != null && !kr_isSubscribed;
// 1. 优先使用 API 返回的 isTryOut 字段
kr_isTrial.value = currentSubscribe.isTryOut;
KRLogUtil.kr_i('步骤1 - API isTryOut 字段: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
KRLogUtil.kr_i('试用状态: ${kr_isTrial.value ? "" : ""}',
tag: 'SubscribeService');
// 2. 如果 API 说不是试用,检查是否有购买记录
if (!kr_isTrial.value) {
final bool kr_isSubscribed = kr_alreadySubscribe.any(
(subscribe) => currentSubscribe.id == subscribe.userSubscribeId
);
KRLogUtil.kr_i('步骤2 - 检查购买记录: $kr_isSubscribed', tag: 'SubscribeService');
// 如果没有购买记录,判断为试用
if (!kr_isSubscribed) {
kr_isTrial.value = true;
KRLogUtil.kr_i('步骤2 - 没有购买记录,判定为试用', tag: 'SubscribeService');
}
}
// 3. 最后检查订阅名称是否包含"试用"关键字(最后的备用方案)
if (!kr_isTrial.value && currentSubscribe.name.contains('试用')) {
kr_isTrial.value = true;
KRLogUtil.kr_i('步骤3 - 订阅名称包含"试用"关键字,判定为试用', tag: 'SubscribeService');
}
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
KRLogUtil.kr_i('当前订阅: ${currentSubscribe.name}(${currentSubscribe.id})', tag: 'SubscribeService');
KRLogUtil.kr_i('API isTryOut: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅记录数: ${kr_alreadySubscribe.length}', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅ID列表: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}', tag: 'SubscribeService');
KRLogUtil.kr_i('✅ 最终试用状态: ${kr_isTrial.value ? "试用" : "付费"}', tag: 'SubscribeService');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
if (kr_isTrial.value) {
// 启动试用倒计时
@@ -445,15 +473,10 @@ class KRSubscribeService {
},
);
final result = await kr_subscribeApi.kr_nodeGroupList();
result.fold(
(error) {
throw Exception('获取节点分组失败: ${error.msg}');
},
(groups) {
kr_nodeGroups.value = groups;
},
);
// 🔧 取消节点分组的概念,不再调用 kr_nodeGroupList
// 直接使用节点列表,通过 is_try_out 字段区分免费/付费节点
kr_nodeGroups.clear();
KRLogUtil.kr_i('已取消节点分组,将直接使用节点列表', tag: 'SubscribeService');
// 保存当前选中的订阅名称
final currentSubscribeID = kr_currentSubscribe.value?.id;
@@ -502,9 +525,26 @@ class KRSubscribeService {
}
}
// 2. 如果没有找到之前的订阅,优先选择已购买的套餐(非试用)
// 2. 如果没有找到之前的订阅,优先选择试用套餐
if (selectedSubscribe == null) {
KRLogUtil.kr_i('开始查找已购买的套餐...', tag: 'SubscribeService');
KRLogUtil.kr_i('开始查找试用套餐...', tag: 'SubscribeService');
for (var subscribe in subscribes) {
KRLogUtil.kr_i('检查订阅: ${subscribe.name}(${subscribe.id}), 是否试用: ${subscribe.isTryOut}',
tag: 'SubscribeService');
if (subscribe.isTryOut) {
selectedSubscribe = subscribe;
KRLogUtil.kr_i('✅ 找到试用套餐,默认选择: ${selectedSubscribe.name}',
tag: 'SubscribeService');
break;
}
}
}
// 3. 如果没有试用套餐,选择已购买的套餐(非试用)
if (selectedSubscribe == null) {
KRLogUtil.kr_i('没有试用套餐,查找已购买的套餐...', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}',
tag: 'SubscribeService');
@@ -524,10 +564,10 @@ class KRSubscribeService {
}
}
// 3. 如果没有已购买的套餐,选择第一个(可能是试用套餐)
// 4. 如果没有找到,选择第一个
if (selectedSubscribe == null) {
selectedSubscribe = subscribes.first;
KRLogUtil.kr_i('没有已购买的套餐,选择第一个: ${selectedSubscribe.name}',
KRLogUtil.kr_i('没有找到匹配的套餐,选择第一个: ${selectedSubscribe.name}',
tag: 'SubscribeService');
}
@@ -545,9 +585,13 @@ class KRSubscribeService {
(nodes) => nodes,
);
// 处理节点列表
// 记录当前节点列表是否包含试用节点
kr_hasTrialNodes.value = nodes.isTryOut;
KRLogUtil.kr_i('节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService');
// 处理节点列表(不使用分组)
final listModel = KrOutboundsList();
listModel.processOutboundItems(nodes.list, kr_nodeGroups);
listModel.processOutboundItems(nodes.list, []);
// 更新UI数据
groupOutboundList.value = listModel.groupOutboundList;
@@ -596,6 +640,7 @@ class KRSubscribeService {
Future<void> kr_clearCutNodeData() async {
kr_isLastDayOfSubscription.value = false;
kr_isTrial.value = false;
kr_hasTrialNodes.value = false;
kr_subscriptionRemainingTime.value = '';
kr_trialRemainingTime.value = '';
@@ -618,4 +663,45 @@ class KRSubscribeService {
/// 获取当前订阅
KRUserAvailableSubscribeItem? get kr_getCurrentSubscribe =>
kr_currentSubscribe.value;
/// 获取免费节点列表(试用节点)
/// 当试用结束时,返回空列表
List<KROutboundItem> get kr_freeNodes {
if (!kr_hasTrialNodes.value) {
return [];
}
// 如果当前是试用状态或有试用节点,返回所有节点作为免费节点
// 如果试用已结束(kr_isTrial=false 且 kr_hasTrialNodes=true),返回空列表
if (kr_isTrial.value) {
KRLogUtil.kr_i('返回免费节点: ${allList.length}', tag: 'SubscribeService');
return allList.toList();
} else {
KRLogUtil.kr_i('试用已结束,不显示免费节点', tag: 'SubscribeService');
return [];
}
}
/// 获取付费节点列表
/// 如果当前不是试用,返回所有节点
/// 如果当前是试用,返回空列表
List<KROutboundItem> get kr_paidNodes {
if (kr_isTrial.value) {
KRLogUtil.kr_i('当前是试用状态,不显示付费节点', tag: 'SubscribeService');
return [];
} else {
KRLogUtil.kr_i('返回付费节点: ${allList.length}', tag: 'SubscribeService');
return allList.toList();
}
}
/// 是否应该显示免费标签页
bool get kr_shouldShowFreeTab {
return kr_hasTrialNodes.value && kr_isTrial.value;
}
/// 是否应该显示付费标签页
bool get kr_shouldShowPaidTab {
return !kr_isTrial.value;
}
}