完善游客模式登录 并且新增设备管理
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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, () => []);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+294
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 添加移除悬浮框的方法
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-9
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 构建快捷键容器
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user