feat: bugfix

This commit is contained in:
2025-12-01 06:54:18 -08:00
parent 1b1b38aba5
commit 86687cac58
22 changed files with 827 additions and 493 deletions
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:flutter/services.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
@@ -15,52 +16,57 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
@override
Widget build(BuildContext context) {
return HIBaseScaffold(
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(40.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildUserInfoSection(),
SizedBox(height: 12.w),
// 验证码输入框
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕)
),
// 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildVerificationCodeField(), // 4. 在这里调用,不修改其内部
],
),
),
SizedBox(height: 20.w),
GestureDetector(
onTap: () {
controller.requestDeleteAccount();
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 12.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.w),
border:
Border.all(color: const Color(0xFFFF2ED1), width: 2),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(40.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildUserInfoSection(),
SizedBox(height: 12.w),
// 验证码输入框
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕)
),
alignment: Alignment.center,
child: Text(
'注销账户',
style: TextStyle(
color: const Color(0xFFFF2ED1),
fontSize: 16.sp,
fontWeight: FontWeight.w700,
// 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildVerificationCodeField(), // 4. 在这里调用,不修改其内部
],
),
),
SizedBox(height: 20.w),
GestureDetector(
onTap: () {
controller.requestDeleteAccount();
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 12.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.w),
border:
Border.all(color: const Color(0xFFFF2ED1), width: 2),
),
alignment: Alignment.center,
child: Text(
'注销账户',
style: TextStyle(
color: const Color(0xFFFF2ED1),
fontSize: 16.sp,
fontWeight: FontWeight.w700,
),
),
),
),
),
/*SizedBox(
/*SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.requestDeleteAccount,
@@ -80,8 +86,9 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
),
),
),*/
SizedBox(height: 20.w),
],
SizedBox(height: 20.w),
],
),
),
),
),
@@ -129,11 +136,9 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
);
}),
Obx(() {
final account = KRAppRunData.getInstance()
.kr_account
.value;
final isDeviceLogin = account != null &&
account.startsWith('9000');
final account = KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin =
account != null && account.startsWith('9000');
final accountText = isDeviceLogin
? '待绑定'
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
@@ -164,11 +169,44 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
/// 构建注册页面的验证码输入框(包含间距)
Widget _buildVerificationCodeField() {
return SizedBox(
height: 50.w, // 固定高度
height: 50.w,
child: TextField(
controller: controller.kr_codeController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.oneTimeCode],
enableSuggestions: false,
autocorrect: false,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) {
var v = value.replaceAll(RegExp(r"\s+"), "");
if (v.length % 2 == 0 && v.isNotEmpty) {
final half = v.length ~/ 2;
final first = v.substring(0, half);
final second = v.substring(half);
if (first == second) {
v = first;
}
}
const maxLen = 6;
if (v.length > maxLen) {
v = v.substring(0, maxLen);
}
if (controller.kr_codeController.text != v) {
controller.kr_codeController.value =
controller.kr_codeController.value.copyWith(
text: v,
selection: TextSelection.collapsed(offset: v.length),
composing: TextRange.empty,
);
}
if (v.isNotEmpty && (v.length >= 6)) {
FocusScope.of(Get.context!).unfocus();
}
},
onSubmitted: (_) {
FocusScope.of(Get.context!).unfocus();
},
style: KrAppTextStyle(
fontSize: 16,
color: Colors.white,
@@ -184,19 +222,20 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25.w),
borderSide: BorderSide(color: Colors.white, width: 2),
borderSide: const BorderSide(color: Colors.white, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25.w),
borderSide: BorderSide(color: Colors.white, width: 2),
borderSide: const BorderSide(color: Colors.white, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25.w),
borderSide: BorderSide(color: Colors.white, width: 2),
borderSide: const BorderSide(color: Colors.white, width: 2),
),
isDense: true,
suffixIconConstraints: BoxConstraints(
maxHeight: 50.w, // 限制最大高度
maxHeight: 50.w,
maxWidth: 190.w,
),
suffixIcon: Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
@@ -212,7 +251,7 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
color: controller.kr_canSendCode.value
? Theme.of(Get.context!).primaryColor
: const Color(0xFFD5D5D5),
borderRadius: BorderRadius.circular(100.r), // 药丸状
borderRadius: BorderRadius.circular(100.r),
),
child: Text(
controller.kr_canSendCode.value
@@ -91,9 +91,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
final kr_isLatency = false.obs;
/// 默认
var kr_cutTag = 'auto'.obs;
var kr_cutSeletedTag = 'auto'.obs;
var kr_coutryText = 'auto'.obs;
var kr_cutTag = ''.obs;
var kr_cutSeletedTag = ''.obs;
var kr_coutryText = ''.obs;
var kr_selectedCountryTag = 'auto'.obs;
void kr_setSelectedCountryTag(String country) {
@@ -216,6 +216,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
}
await _kr_prepareCountrySelectionBeforeStart();
final selectedAfter =
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG_autoConnect: ${selectedAfter ?? ''}',
tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_currentNodeName_autoConnect: ${kr_currentNodeName.value}',
tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_cutTag_autoConnect: ${kr_cutTag.value}',
tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_cutSeletedTag_autoConnect: ${kr_cutSeletedTag.value}',
tag: 'HomeController');
await kr_performNodeSwitch(selectedAfter!);
await KRSingBoxImp.instance.kr_start();
KRLogUtil.kr_i('闪连自动连接执行完成', tag: 'QuickConnect');
} catch (e) {
@@ -659,9 +670,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
break;
case KRSubscribeServiceStatus.kr_success:
KRLogUtil.kr_i('订阅服务成功', tag: 'HomeController');
kr_cutTag.value = 'auto';
kr_cutSeletedTag.value = 'auto';
kr_currentNodeName.value = "auto";
kr_cutTag.value = '';
kr_cutSeletedTag.value = '';
kr_currentNodeName.value = "";
if (kr_currentListStatus.value != KRHomeViewsListStatus.kr_none) {
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
} else {
@@ -779,8 +790,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
break;
case SingboxStopping():
KRLogUtil.kr_i('🟠 状态: 正在停止', tag: 'HomeController');
// 取消连接超时处理
_cancelConnectionTimeout();
kr_connectText.value = AppTranslations.kr_home.disconnecting;
kr_isConnected.value = false;
kr_currentSpeed.value = "--";
@@ -885,6 +894,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController');
if (kDebugMode) {}
await _kr_prepareCountrySelectionBeforeStart();
final selectedAfter =
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG: ${selectedAfter ?? ''}',
tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_currentNodeName: ${kr_currentNodeName.value}',
tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_cutTag: ${kr_cutTag.value}',
tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_cutSeletedTag: ${kr_cutSeletedTag.value}',
tag: 'HomeController');
await kr_performNodeSwitch(selectedAfter!);
await KRSingBoxImp.instance.kr_start();
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
if (kDebugMode) {}
@@ -958,6 +978,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verify',
tag: 'CountrySelect');
kr_currentNodeName.value = best;
kr_cutTag.value = best;
kr_cutSeletedTag.value = best;
kr_updateConnectionInfo();
return;
@@ -974,6 +996,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verifyA',
tag: 'CountrySelect');
kr_currentNodeName.value = bestAfterTest;
kr_cutTag.value = bestAfterTest;
kr_cutSeletedTag.value = bestAfterTest;
kr_updateConnectionInfo();
return;
@@ -991,6 +1015,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verifyB',
tag: 'CountrySelect');
kr_currentNodeName.value = fallback;
kr_cutTag.value = fallback;
kr_cutSeletedTag.value = fallback;
kr_updateConnectionInfo();
} else {
@@ -1027,6 +1053,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('写入后校验 SELECTED_NODE_TAG: $verify2',
tag: 'CountrySelect');
kr_currentNodeName.value = bestInCountry;
kr_cutTag.value = bestInCountry;
kr_cutSeletedTag.value = bestInCountry;
kr_updateConnectionInfo();
} else {
@@ -1084,22 +1112,33 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
void _kr_handleSelectorProxy(dynamic element, List<dynamic> allGroups) {
try {
KRLogUtil.kr_d(
'处理选择器代理 - 当前选择: ${element.selected}, 用户选择: ${kr_cutTag.value}',
'处理选择器代理 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}, 切换中: $_isSwitchingNode',
tag: 'HomeController');
// 🔧 关键修复:仅更新 UI 状态,不要触发重新选择,避免死循环
// 更新 kr_cutSeletedTag 以反映实际选中的节点
if (element.selected.isNotEmpty) {
kr_cutSeletedTag.value = element.selected;
// 🔧 关键修复:如果正在切换节点中,不要用内核返回值覆盖 UI
// 这是导致"跳两次"问题的根本原因
if (_isSwitchingNode) {
KRLogUtil.kr_d('⏳ 节点切换进行中,跳过内核状态同步', tag: 'HomeController');
// 只更新延迟值,不更新选中状态
_kr_updateNodeLatency(element);
return;
}
// 如果用户手动选择了节点(不是auto)
if (kr_cutTag.value != "auto") {
// 🔧 修复:用户手动选择时,不要用内核返回的 selected 覆盖用户选择
// 只有当内核返回的节点与用户选择一致时,才更新 kr_cutSeletedTag
if (element.selected == kr_cutTag.value) {
kr_cutSeletedTag.value = element.selected;
}
_kr_handleManualMode(element);
return;
}
// 默认auto模式处理
// 默认auto模式处理 - 此时可以用内核返回值更新 UI
if (element.selected.isNotEmpty) {
kr_cutSeletedTag.value = element.selected;
}
_kr_handleAutoMode(element, allGroups);
} catch (e) {
KRLogUtil.kr_e('处理选择器代理出错: $e', tag: 'HomeController');
@@ -1109,15 +1148,25 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
/// 处理手动模式
void _kr_handleManualMode(dynamic element) {
try {
KRLogUtil.kr_d('处理手动模式 - 选择: ${element.selected}', tag: 'HomeController');
KRLogUtil.kr_d('处理手动模式 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}', tag: 'HomeController');
// 🔧 关键修复:仅更新 UI 状态,不要重新选择节点,避免死循环
kr_cutSeletedTag.value = element.selected;
// 更新延迟值
// 🔧 关键修复:不要用内核返回的 selected 覆盖用户手动选择
// 只有当内核返回的节点与用户选择一致时,才更新相关状态
// 这可以防止 UI 跳动
// 更新延迟值(这个始终需要更新)
_kr_updateNodeLatency(element);
kr_currentNodeName.value =
kr_truncateText(element.selected, maxLength: 25);
// kr_moveToSelectedNode();
// 只有当内核确认切换成功时,才更新 UI 状态
if (element.selected == kr_cutTag.value) {
kr_cutSeletedTag.value = element.selected;
kr_currentNodeName.value = kr_truncateText(element.selected, maxLength: 25);
// kr_moveToSelectedNode();
KRLogUtil.kr_d('✅ 内核确认节点切换成功: ${element.selected}', tag: 'HomeController');
} else {
// 内核返回的节点与用户选择不一致,保持用户选择的显示
KRLogUtil.kr_d('⏳ 等待内核切换到用户选择的节点: ${kr_cutTag.value}', tag: 'HomeController');
}
} catch (e) {
KRLogUtil.kr_e('处理手动模式出错: $e', tag: 'HomeController');
}
@@ -1315,12 +1364,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 🔒 节流控制:2秒内的重复切换请求直接忽略
final now = DateTime.now();
if (_lastSwitchTime != null &&
now.difference(_lastSwitchTime!) < _switchThrottleDuration) {
final remainingTime = _switchThrottleDuration.inMilliseconds -
now.difference(_lastSwitchTime!).inMilliseconds;
KRLogUtil.kr_w('⚠️ 切换过于频繁,请等待 ${remainingTime}ms',
tag: 'HomeController');
if (_lastSwitchTime != null && now.difference(_lastSwitchTime!) < _switchThrottleDuration) {
final remainingTime = _switchThrottleDuration.inMilliseconds - now.difference(_lastSwitchTime!).inMilliseconds;
KRLogUtil.kr_w('⚠️ 切换过于频繁,请等待 ${remainingTime}ms', tag: 'HomeController');
KRCommonUtil.kr_showToast('切换过于频繁,请稍后再试');
return false;
}
@@ -1347,9 +1393,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 🔧 修复:保存节点选择以便VPN启动时应用
KRLogUtil.kr_i('💾 保存节点选择以便稍后应用: $tag', tag: 'HomeController');
KRSecureStorage()
.kr_saveData(key: 'SELECTED_NODE_TAG', value: tag)
.then((_) {
KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag).then((_) {
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'HomeController');
}).catchError((e) {
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'HomeController');
@@ -1358,91 +1402,93 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
return true;
}
// 4. VPN已连接,需要重启VPN以断开现有连接并应用新节点
// 4. VPN已连接,使用热切换方式(selectOutbound)切换节点
try {
KRLogUtil.kr_i('🔌 VPN已连接,将重启VPN以断开现有连接: $tag', tag: 'HomeController');
KRLogUtil.kr_i('🔌 VPN已连接,使用热切换方式切换节点: $tag', tag: 'HomeController');
// 🔧 诊断:打印当前活动组信息
KRLogUtil.kr_i(
'📊 当前活动组数量: ${KRSingBoxImp.instance.kr_activeGroups.length}',
tag: 'HomeController');
for (var group in KRSingBoxImp.instance.kr_activeGroups) {
KRLogUtil.kr_i(
'📋 活动组: tag=${group.tag}, type=${group.type}, 节点数=${group.items.length}',
tag: 'HomeController');
for (var item in group.items) {
if (item.tag == tag) {
KRLogUtil.kr_i('✅ 找到目标节点: ${item.tag}', tag: 'HomeController');
}
}
}
// 🔧 修复:VPN已连接时,设置延迟为-1(切换中状态),显示"正在连接"
// 🔧 设置切换中状态,显示"正在连接"
kr_currentNodeLatency.value = -1;
kr_isLatency.value = true; // 显示加载动画
// 🔧 关键修复:保存新节点选择
// 🔧 保存新节点选择
KRLogUtil.kr_i('💾 保存新节点选择: $tag', tag: 'HomeController');
await KRSecureStorage()
.kr_saveData(key: 'SELECTED_NODE_TAG', value: tag);
await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag);
// 🔧 方案A优化:重启VPN连接以断开所有现有长连接
KRLogUtil.kr_i('🔄 [优化] 停止VPN连接以断开现有连接...', tag: 'HomeController');
await KRSingBoxImp.instance.kr_stop(); // 先停止VPN(已跳过DNS恢复)
// 🚀 核心改进:使用 selectOutbound 进行热切换(参考 hiddify-app
// 优势:不重启VPN,保持连接状态,切换瞬间完成,VPN开关不闪烁
KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...', tag: 'HomeController');
// 🚀 优化:减少等待时间(DNS操作已优化,无需过长等待)
KRLogUtil.kr_i('⏳ [优化] 等待VPN完全停止(800ms...', tag: 'HomeController');
await Future.delayed(
const Duration(milliseconds: 800)); // 从1500ms减少到800ms
// 🔧 关键修复:确定正确的 selector 组 tag
// selectOutbound(groupTag, outboundTag) - 第一个参数是组的tag,不是节点的tag
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
String selectorGroupTag = 'select'; // 默认值
KRLogUtil.kr_i('🔄 [优化] 启动VPN并应用新节点: $tag', tag: 'HomeController');
await KRSingBoxImp.instance.kr_start(); // 重新启动VPN(已跳过DNS备份)
// 查找 selector 类型的组
for (var group in activeGroups) {
if (group.type == ProxyType.selector) {
selectorGroupTag = group.tag;
KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag', tag: 'HomeController');
break;
}
}
// 🚀 优化:减少等待时间(DNS操作已优化)
KRLogUtil.kr_i('⏳ [优化] 等待VPN完全启动(1200ms...', tag: 'HomeController');
await Future.delayed(
const Duration(milliseconds: 1200)); // 从2500ms减少到1200ms
KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")', tag: 'HomeController');
// 后台切换成功,更新UI
// 调用 sing-box 的 selectOutbound API
final result = await KRSingBoxImp.instance.kr_singBox
.selectOutbound(selectorGroupTag, tag)
.run();
// 处理切换结果
result.fold(
(error) {
// 切换失败
KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error', tag: 'HomeController');
throw Exception('节点切换失败: $error');
},
(_) {
// 切换成功
KRLogUtil.kr_i('✅ selectOutbound 调用成功', tag: 'HomeController');
},
);
// 后台切换成功,立即更新UI(乐观更新)
kr_cutSeletedTag.value = tag;
kr_updateConnectionInfo();
kr_moveToSelectedNode();
// kr_moveToSelectedNode();
// 🚀 优化:减少验证等待时间
KRLogUtil.kr_i('⏳ [优化] 等待活动组更新(300ms...', tag: 'HomeController');
await Future.delayed(
const Duration(milliseconds: 300)); // 从500ms减少到300ms
// 🔧 短暂等待以确保内核状态同步(相比重启,等待时间大幅缩短)
KRLogUtil.kr_i('⏳ [热切换] 等待内核状态同步(200ms...', tag: 'HomeController');
await Future.delayed(const Duration(milliseconds: 200));
// 🚀 方案A增强:验证节点是否真正切换成功
KRLogUtil.kr_i('🔍 [增强] 验证节点切换是否成功...', tag: 'HomeController');
// 🔍 验证节点是否真正切换成功
KRLogUtil.kr_i('🔍 [验证] 检查节点切换结果...', tag: 'HomeController');
try {
// 刷新活动组信息
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
final selectGroup = activeGroups.firstWhere(
(group) => group.tag == 'select',
orElse: () => throw Exception('未找到 select 组'),
final updatedGroups = KRSingBoxImp.instance.kr_activeGroups;
final selectGroup = updatedGroups.firstWhere(
(group) => group.type == ProxyType.selector,
orElse: () => throw Exception('未找到 selector 组'),
);
KRLogUtil.kr_i('📊 [增强] Select组当前选中: ${selectGroup.selected}',
tag: 'HomeController');
KRLogUtil.kr_i('📊 [增强] 目标节点: $tag', tag: 'HomeController');
KRLogUtil.kr_i('📊 [验证] ${selectGroup.tag}组当前选中: ${selectGroup.selected}', tag: 'HomeController');
KRLogUtil.kr_i('📊 [验证] 目标节点: $tag', tag: 'HomeController');
if (selectGroup.selected != tag) {
KRLogUtil.kr_w('⚠️ [增强] 节点选择验证失败,实际选中: ${selectGroup.selected}',
tag: 'HomeController');
KRLogUtil.kr_w('⚠️ [验证] 节点选择验证失败,实际选中: ${selectGroup.selected}', tag: 'HomeController');
// 不抛出异常,但记录警告
} else {
KRLogUtil.kr_i('✅ [增强] 节点选择验证成功!', tag: 'HomeController');
KRLogUtil.kr_i('✅ [验证] 节点选择验证成功!', tag: 'HomeController');
}
} catch (e) {
KRLogUtil.kr_w('⚠️ [增强] 节点验证过程出错: $e', tag: 'HomeController');
KRLogUtil.kr_w('⚠️ [验证] 节点验证过程出错: $e', tag: 'HomeController');
}
// 更新延迟信息
_kr_updateLatencyOnConnected();
KRLogUtil.kr_i('✅ 节点切换成功(已重启VPN断开旧连接): $tag', tag: 'HomeController');
KRLogUtil.kr_i('✅ 节点切换成功VPN保持连接: $tag', tag: 'HomeController');
return true;
} catch (switchError) {
// 后台切换失败,恢复到原节点
KRLogUtil.kr_e('❌ 后台节点切换失败: $switchError', tag: 'HomeController');
@@ -1454,8 +1500,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 恢复原节点选择
try {
await KRSecureStorage()
.kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag);
await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag);
} catch (e) {
KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'HomeController');
}
@@ -55,7 +55,7 @@ class CustomCircleRoute extends PageRouteBuilder {
required this.child,
required this.startOffset,
required this.transitionColor,
this.customDuration = const Duration(milliseconds: 1500),
this.customDuration = const Duration(milliseconds: 250),
}) : super(
opaque: false,
transitionDuration: customDuration,
@@ -162,7 +162,7 @@ class _HISubscriptionCornerButtonState extends State<HISubscriptionCornerButton>
child: KRPurchaseMembershipView(),
startOffset: startOffset,
transitionColor: transitionColor,
customDuration: const Duration(milliseconds: 1500),
customDuration: const Duration(milliseconds: 500),
),
).then((_) {
// 页面返回后恢复默认主题色
@@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../controllers/kr_invite_controller.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
@@ -15,7 +16,6 @@ class KRInviteView extends GetView<KRInviteController> {
@override
Widget build(BuildContext context) {
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
return HIBaseScaffold(
@@ -35,7 +35,8 @@ class KRInviteView extends GetView<KRInviteController> {
// 🟢 第一行:奖励说明
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(25.r),
@@ -68,13 +69,14 @@ class KRInviteView extends GetView<KRInviteController> {
SizedBox(height: 26.w),
// 🟢 第二行:我的邀请码
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
padding:
EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2.0),
borderRadius: BorderRadius.circular(1000.r),
),
child: Obx(
() => Row(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
@@ -113,12 +115,27 @@ class KRInviteView extends GetView<KRInviteController> {
),
onPressed: () {
if (controller.kr_referCode.value.isNotEmpty) {
Clipboard.setData(
ClipboardData(text: controller.kr_referCode.value),
);
KRCommonUtil.kr_showToast(
AppTranslations.kr_invite.inviteCodeCopied,
);
if (GetPlatform.isIOS) {
final code = controller.kr_referCode.value;
final text = '#您的好友邀请您使用Hi快网络加速器\n'
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
'$code\n'
'您和您的好友将会分别获得3天免费时长\n\n'
'点击此处进入下载页面\n'
'或在浏览器输入hifastvpn.com下载#';
Share.share(
text,
subject: '直接分享Hi快VPN邀请链接',
);
} else {
Clipboard.setData(
ClipboardData(
text: controller.kr_referCode.value),
);
KRCommonUtil.kr_showToast(
AppTranslations.kr_invite.inviteCodeCopied,
);
}
}
},
),
@@ -143,24 +160,29 @@ class KRInviteView extends GetView<KRInviteController> {
TextField(
controller: controller.otherInviteCodeController,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
decoration: InputDecoration(
hintText: '填入邀请人邀请码兑换免费时长...',
hintStyle: const TextStyle(color: Color(0xFFA6A6A6)),
filled: true,
fillColor: Colors.transparent,
contentPadding: EdgeInsets.symmetric(horizontal: 22.w),
contentPadding:
EdgeInsets.symmetric(horizontal: 22.w),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
borderSide: const BorderSide(
color: Colors.white, width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
borderSide: const BorderSide(
color: Colors.white, width: 2.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(1000.r),
borderSide: const BorderSide(color: Colors.white, width: 2.0),
borderSide: const BorderSide(
color: Colors.white, width: 2.0),
),
constraints: BoxConstraints(maxHeight: 50.h),
),
@@ -199,8 +221,7 @@ class KRInviteView extends GetView<KRInviteController> {
),
// 5. 将 HIHelpEntrance 作为 Stack 的直接子元素,它将恢复其原有的绝对定位能力
if (!isKeyboardVisible)
const HIHelpEntrance(),
if (!isKeyboardVisible) const HIHelpEntrance(),
],
),
);
@@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_control
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:flutter/foundation.dart';
import 'package:kaer_with_panels/app/widgets/kr_subscription_expiry_text.dart';
import 'package:flutter/services.dart';
class KRLoginView extends GetView<KRLoginController> {
const KRLoginView({super.key});
@@ -27,23 +28,29 @@ class KRLoginView extends GetView<KRLoginController> {
resizeToAvoidBottomInset: true,
child: Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 40.w,
),
child: Column(
children: [
// Text(
// '${controller.kr_loginStatus.value}',
// style: TextStyle(color: Colors.white), // 使用 TextStyle()
// ),
SizedBox(height: 20.w),
_buildUserInfoSection(),
SizedBox(height: 12.w),
_buildContentByEntry(),
SizedBox(height: 100.w), // 为底部帮助按钮留出空间
],
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
FocusScope.of(context).unfocus();
},
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 40.w,
),
child: Column(
children: [
// Text(
// '${controller.kr_loginStatus.value}',
// style: TextStyle(color: Colors.white), // 使用 TextStyle()
// ),
SizedBox(height: 20.w),
_buildUserInfoSection(),
SizedBox(height: 12.w),
_buildContentByEntry(),
SizedBox(height: 100.w), // 为底部帮助按钮留出空间
],
),
),
),
),
@@ -79,11 +86,9 @@ class KRLoginView extends GetView<KRLoginController> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
final account = KRAppRunData.getInstance()
.kr_account
.value;
final isDeviceLogin = account != null &&
account.startsWith('9000');
final account = KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin =
account != null && account.startsWith('9000');
final accountText = isDeviceLogin
? '待绑定'
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
@@ -270,6 +275,41 @@ class KRLoginView extends GetView<KRLoginController> {
height: 50.w, // 固定高度
child: TextField(
controller: controller.codeController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.oneTimeCode],
enableSuggestions: false,
autocorrect: false,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) {
var v = value.replaceAll(RegExp("\\s+"), "");
if (v.length % 2 == 0 && v.isNotEmpty) {
final half = v.length ~/ 2;
final first = v.substring(0, half);
final second = v.substring(half);
if (first == second) {
v = first;
}
}
const maxLen = 6;
if (v.length > maxLen) {
v = v.substring(0, maxLen);
}
if (controller.codeController.text != v) {
controller.codeController.value =
controller.codeController.value.copyWith(
text: v,
selection: TextSelection.collapsed(offset: v.length),
composing: TextRange.empty,
);
}
if (v.isNotEmpty && (v.length >= 6)) {
FocusScope.of(Get.context!).unfocus();
}
},
onSubmitted: (_) {
FocusScope.of(Get.context!).unfocus();
},
style: KrAppTextStyle(
fontSize: 16,
color: Colors.white,
@@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart';
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
import '../../../widgets/kr_simple_loading.dart';
class KRMessageView extends GetView<KRMessageController> {
const KRMessageView({super.key});
@@ -25,62 +26,77 @@ class KRMessageView extends GetView<KRMessageController> {
children: [
// 主要内容区域
Obx(() {
return EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
onLoad: controller.kr_onLoadMore,
header: ClassicHeader(
dragText: '下拉刷新',
armedText: '释放刷新',
readyText: '正在刷新...',
processingText: '正在刷新...',
processedText: '刷新成功',
failedText: '刷新失败',
messageText: '最后更新于 %T',
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
),
// 3. 添加 Footer 以显示上拉加载的UI提示
footer: ClassicFooter(
dragText: '上拉加载',
armedText: '释放加载',
readyText: '正在加载...',
processingText: '正在加载...',
processedText: '加载成功',
failedText: '加载失败',
noMoreText: '没有更多数据了',
messageText: '最后更新于 %T',
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
),
child: Padding(
padding: EdgeInsets.only(right: 0.w, bottom: 90.w), // 为HIHelpEntrance留出空间
child: HiFixedScrollbar(
controller: scrollController,
isShowScrollbar: controller.kr_messages.length > 0,
child: ListView.builder(
controller: scrollController,
padding: EdgeInsets.symmetric(horizontal: 40.w),
itemCount: controller.kr_messages.length,
itemBuilder: (context, index) {
final message = controller.kr_messages[index];
final collapsibleItemData = HICollapsibleItem(
title: message.title,
content: [message.content],
);
return Padding(
padding: EdgeInsets.only(bottom: 10.w),
child: HICollapsibleItemWidget(item: collapsibleItemData),
);
},
return Column(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: 90.w),
child: EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
onLoad: controller.kr_onLoadMore,
header: ClassicHeader(
dragText: '下拉刷新',
armedText: '释放刷新',
readyText: '正在刷新...',
processingText: '正在刷新...',
processedText: '刷新成功',
failedText: '刷新失败',
messageText: '最后更新于 %T',
textStyle:
TextStyle(color: Colors.white.withOpacity(0.7)),
messageStyle: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12.sp),
iconTheme:
IconThemeData(color: Colors.white.withOpacity(0.7)),
),
footer: ClassicFooter(
dragText: '上拉加载',
armedText: '释放加载',
readyText: '正在加载...',
processingText: '正在加载...',
processedText: '加载成功',
failedText: '加载失败',
noMoreText: '没有更多数据了',
messageText: '最后更新于 %T',
textStyle:
TextStyle(color: Colors.white.withOpacity(0.7)),
messageStyle: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12.sp),
iconTheme:
IconThemeData(color: Colors.white.withOpacity(0.7)),
),
child: Padding(
padding: EdgeInsets.only(right: 0.w),
child: HiFixedScrollbar(
controller: scrollController,
child: ListView.builder(
controller: scrollController,
padding: EdgeInsets.symmetric(horizontal: 40.w),
itemCount: controller.kr_messages.length,
itemBuilder: (context, index) {
final message = controller.kr_messages[index];
final collapsibleItemData = HICollapsibleItem(
title: message.title,
content: [message.content],
);
return Padding(
padding: EdgeInsets.only(bottom: 10.w),
child: HICollapsibleItemWidget(
item: collapsibleItemData),
);
},
),
),
),
),
),
),
),
],
);
},),
}),
// 底部帮助入口
const HIHelpEntrance(),
],
@@ -25,13 +25,10 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
children: [
Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
padding: EdgeInsets.symmetric(horizontal: 0.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 状态图标
// _buildStatusIcon(),
SizedBox(height: 122.h),
// 状态文本
_buildStatusText(),
SizedBox(height: 16.h),
@@ -92,14 +92,14 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
padding:
EdgeInsets.only(bottom: 8.0),
child: SizedBox(
height: 140.0,
height: 130.0,
child: _kr_buildPlanOptionCard(
plan,
controller
.kr_selectedPlanIndex.value,
discountIndex,
context,
index == 0,
index,
),
),
);
@@ -167,11 +167,12 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
int planIndex,
int? discountIndex,
BuildContext context,
bool isFirst,
int index,
) {
// 由于移除了 ObxisSelected 的值在构建时就固定了。
// 它将不再响应用户的点击事件来改变UI。
// 根据您的要求,isSelected 的值与 isFirst 相反。
bool isFirst = 0 == index;
bool isSelected = !isFirst;
return GestureDetector(
@@ -190,16 +191,16 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
child: ClipRRect(
borderRadius: BorderRadius.circular(38.0), // 内部圆角
child: Container(
padding: EdgeInsets.fromLTRB(45.0, 10.0, 0.0, 10.0),
padding: EdgeInsets.fromLTRB(45.0, 0.0, 0.0, 0.0),
decoration: BoxDecoration(
color: isSelected ? Colors.black : Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(38.0),
),
child: Stack(
alignment: Alignment.centerLeft,
clipBehavior: Clip.none,
children: [
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -214,25 +215,80 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
Text(
'¥${controller.kr_getPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}',
style: TextStyle(
fontSize: 40,
height: 1,
fontWeight: FontWeight.w600,
Row(
mainAxisSize: MainAxisSize.min,
children: [
KrLocalImage(
imageName: 'money-icon',
imageType: ImageType.svg,
width: 36,
height: 36,
color: isSelected
? Colors.white
: Theme.of(context).textTheme.bodyMedium?.color,
)),
Text(
'约¥${controller.kr_getDayPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}/天',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
SizedBox(width: 2),
Text(
controller
.kr_getPlanPrice(plan,
discountIndex: discountIndex)
.toStringAsFixed(2),
style: TextStyle(
fontSize: 40,
height: 1,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.white
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.white
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
SizedBox(width: 2),
KrLocalImage(
imageName: 'money-icon',
imageType: ImageType.svg,
width: 16,
height: 16,
color: isSelected
? Colors.white
: Theme.of(context).textTheme.bodyMedium?.color,
)),
),
SizedBox(width: 2),
Text(
'${controller.kr_getDayPlanPrice(plan, discountIndex: discountIndex).toStringAsFixed(2)}/天',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.white
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
],
),
]),
if (!isFirst)
Positioned(
@@ -242,7 +298,7 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
angle: 0.785398, // 45度角 (π/4)
child: Container(
// 调整位置,让标签的一部分显示在容器外
transform: Matrix4.translationValues(30, -10, 0),
transform: Matrix4.translationValues(40, -10, 0),
width: 130, // 适当的宽度
height: 20, // 固定高度
decoration: BoxDecoration(
@@ -254,7 +310,9 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
style: TextStyle(
color: Colors.black, // 白色文字更清晰
fontSize: 14,
fontWeight: FontWeight.w300,
fontWeight: index == 1
? FontWeight.w300
: FontWeight.w600,
),
textAlign: TextAlign.center,
),