feat: bugfix
This commit is contained in:
@@ -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,
|
||||
) {
|
||||
// 由于移除了 Obx,isSelected 的值在构建时就固定了。
|
||||
// 它将不再响应用户的点击事件来改变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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user