初始化提交

This commit is contained in:
2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_country_selector_controller.dart';
class KRCountrySelectorBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRCountrySelectorController>(
() => KRCountrySelectorController(),
);
}
}
@@ -0,0 +1,43 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
class KRCountrySelectorController extends GetxController {
// 使用 KRCountry 枚举来加载国家
final RxList<KRCountry> kr_countries = <KRCountry>[].obs;
// 当前选中的国家
final Rx<KRCountry> kr_selectedCountry = KRCountry.cn.obs;
@override
void onInit() {
super.onInit();
kr_selectedCountry.value = KRCountryUtil.kr_currentCountry.value;
kr_loadCountries();
}
// 加载国家数据
void kr_loadCountries() {
kr_countries.value = KRCountryUtil.kr_getSupportedCountries();
}
// 选择国家
Future<void> kr_selectCountry(KRCountry country) async {
kr_selectedCountry.value = country;
// try {
// await KRSingBoxImp().kr_updateCountry(country);
// // Get.back();
// } catch (err) {
// }
}
@override
void onClose() {
// TODO: implement onClose
super.onClose();
KRSingBoxImp().kr_updateCountry(kr_selectedCountry.value);
}
}
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../controllers/kr_country_selector_controller.dart';
class KRCountrySelectorView extends GetView<KRCountrySelectorController> {
const KRCountrySelectorView({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,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_setting.countrySelector,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => ListView.separated(
padding: EdgeInsets.all(16.r),
itemCount: controller.kr_countries.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final country = controller.kr_countries[index];
return _kr_buildCountryCard(country, context);
},
),
),
),
],
),
),
);
}
// 构建国家卡片
Widget _kr_buildCountryCard(KRCountry country, BuildContext context) {
return Obx(
() => InkWell(
onTap: () => controller.kr_selectCountry(country),
child: Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
// 国家图标
KRCountryFlag(
countryCode: country.kr_code,
width: 24.r,
height: 24.r,
),
SizedBox(width: 12.w),
// 国家名称
Text(
KRCountryUtil.kr_getCountryName(country),
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
// 选中标记
if (controller.kr_selectedCountry.value == country)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.r,
),
],
),
),
),
);
}
}
@@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/kr_crisp_controller.dart';
/// Crisp 聊天绑定
class KRCrispBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<KRCrispController>(() => KRCrispController());
}
}
@@ -0,0 +1,192 @@
import 'package:get/get.dart';
// import 'package:crisp_sdk/crisp_sdk.dart'; // 暂时注释掉
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'dart:io' show Platform;
import 'dart:async';
import 'package:permission_handler/permission_handler.dart';
import '../../../utils/kr_device_util.dart';
/// Crisp 聊天控制器
class KRCrispController extends GetxController {
// Crisp 控制器
// CrispController? crispController; // 暂时注释掉
dynamic crispController; // 临时类型
// 加载状态
final RxBool kr_isLoading = true.obs;
// 初始化完成状态
final RxBool kr_isInitialized = false.obs;
// 录音权限状态
final RxBool kr_hasRecordPermission = false.obs;
// 用于取消异步操作的订阅
Completer<void>? _kr_initializationCompleter;
bool _kr_isDisposed = false;
@override
void onInit() {
super.onInit();
_kr_prepareInitialization();
}
@override
void onReady() {
super.onReady();
}
/// 检查录音权限
Future<void> _kr_checkRecordPermission() async {
if (_kr_isDisposed) return;
try {
if (Platform.isAndroid) {
final status = await Permission.microphone.request();
if (!_kr_isDisposed) {
kr_hasRecordPermission.value = status.isGranted;
if (!status.isGranted) {
print('麦克风权限未授予,状态: $status');
}
}
} else {
if (!_kr_isDisposed) {
kr_hasRecordPermission.value = true;
}
}
} catch (e) {
if (!_kr_isDisposed) {
print('检查录音权限时出错: $e');
kr_hasRecordPermission.value = false;
}
}
}
/// 准备初始化
Future<void> _kr_prepareInitialization() async {
if (_kr_isDisposed) return;
_kr_initializationCompleter = Completer<void>();
try {
kr_isLoading.value = true;
await kr_initializeCrisp();
if (!_kr_isDisposed) {
kr_isInitialized.value = true;
}
} catch (e) {
print('初始化 Crisp 时出错: $e');
if (!_kr_isDisposed) {
kr_isInitialized.value = false;
}
} finally {
if (!_kr_isDisposed) {
kr_isLoading.value = false;
}
_kr_initializationCompleter?.complete();
}
}
/// 初始化 Crisp
Future<void> kr_initializeCrisp() async {
if (_kr_isDisposed) return;
try {
final appData = KRAppRunData();
final currentLanguage = KRLanguageUtils.getCurrentLanguageCode();
final userEmail = appData.kr_account ?? '';
// 获取设备 ID
final deviceId = await KRDeviceUtil().kr_getDeviceId();
final identifier = userEmail.isNotEmpty ? userEmail : deviceId;
// 根据当前语言设置对应的 locale
String locale = 'en';
if (currentLanguage == 'zh_CN') {
locale = 'zh';
} else if (currentLanguage == 'zh_TW') {
locale = 'zh-tw';
}
if (_kr_isDisposed) return;
// 初始化 Crisp 控制器
// crispController = CrispController(
// websiteId: AppConfig.getInstance().kr_website_id,
// locale: locale,
// );
// if (_kr_isDisposed) {
// crispController = null;
// return;
// }
// // 设置用户信息
// crispController?.register(
// user: CrispUser(
// email: identifier,
// nickname: identifier,
// ),
// );
// if (_kr_isDisposed) {
// crispController = null;
// return;
// }
// // 设置会话数据
// crispController?.setSessionData({
// 'platform': Platform.isAndroid
// ? 'android'
// : Platform.isIOS
// ? 'ios'
// : Platform.isWindows
// ? 'windows'
// : Platform.isMacOS
// ? 'macos'
// : 'unknown',
// 'language': currentLanguage,
// 'app_version': '1.0.0',
// 'has_microphone_permission': kr_hasRecordPermission.value.toString(),
// });
// 临时设置一个占位符
crispController = 'placeholder';
print('Crisp 初始化完成,麦克风权限状态: ${kr_hasRecordPermission.value}');
} catch (e) {
print('初始化 Crisp 时出错: $e');
crispController = null;
rethrow;
}
}
@override
void onClose() {
_kr_isDisposed = true;
kr_cleanupResources();
super.onClose();
}
/// 清理 Crisp 资源
Future<void> kr_cleanupResources() async {
try {
// 等待初始化完成
if (_kr_initializationCompleter != null && !_kr_initializationCompleter!.isCompleted) {
await _kr_initializationCompleter!.future;
}
if (kr_isInitialized.value) {
// 清理 Crisp 会话
crispController = null;
kr_isInitialized.value = false;
kr_isLoading.value = false;
}
} catch (e) {
print('清理 Crisp 资源时出错: $e');
}
}
}
+133
View File
@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// import 'package:crisp_sdk/crisp_sdk.dart'; // 暂时注释掉
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/kr_crisp_controller.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../widgets/kr_simple_loading.dart';
/// Crisp 客服聊天视图
class KRCrispView extends GetView<KRCrispController> {
const KRCrispView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
await controller.kr_cleanupResources();
return true;
},
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: _kr_buildAppBar(context),
body: _kr_buildBody(context),
),
);
}
/// 构建导航栏
PreferredSizeWidget _kr_buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: Theme.of(context).cardColor,
elevation: 0,
title: Text(
AppTranslations.kr_userInfo.customerService,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => _kr_handleBack(),
),
);
}
/// 构建主体内容
Widget _kr_buildBody(BuildContext context) {
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Obx(() {
if (controller.kr_isLoading.value) {
return _kr_buildLoadingView(context, '');
}
if (controller.kr_isInitialized.value && controller.crispController != null) {
// return CrispView(
// crispController: controller.crispController!,
// clearCache: true,
// onSessionIdReceived: _kr_onSessionIdReceived,
// );
return _kr_buildPlaceholderView(context);
}
return _kr_buildLoadingView(context, '');
}),
);
}
/// 构建加载视图
Widget _kr_buildLoadingView(BuildContext context, String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
KRSimpleLoading(
color: Colors.blue,
size: 50.0,
),
if (message.isNotEmpty) SizedBox(height: 16.sp),
if (message.isNotEmpty) Text(
message,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
/// 处理返回事件
Future<void> _kr_handleBack() async {
await controller.kr_cleanupResources();
Get.back();
}
/// 构建占位视图
Widget _kr_buildPlaceholderView(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat_bubble_outline,
size: 64.sp,
color: Colors.grey,
),
SizedBox(height: 16.sp),
Text(
'客服功能暂时不可用',
style: KrAppTextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
);
}
/// 处理会话 ID 接收事件
void _kr_onSessionIdReceived(String sessionId) {
debugPrint('Crisp 会话 ID: $sessionId');
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_delete_account_controller.dart';
class KrDeleteAccountBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRDeleteAccountController>(
() => KRDeleteAccountController(),
);
}
}
@@ -0,0 +1,109 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import '../../../localization/app_translations.dart';
class KRDeleteAccountController extends GetxController {
// 验证码输入框控制器
final TextEditingController kr_codeController = TextEditingController();
// 验证码输入框是否有文本
final RxBool kr_codeHasText = false.obs;
// 是否可以发送验证码
final RxBool kr_canSendCode = true.obs;
// 倒计时秒数
final RxInt kr_countdown = 60.obs;
// 定时器
Timer? _timer;
// API 实例
final KRAuthApi _authApi = KRAuthApi();
@override
void onInit() {
super.onInit();
// 监听验证码输入框文本变化
kr_codeController.addListener(() {
kr_codeHasText.value = kr_codeController.text.isNotEmpty;
});
}
@override
void onClose() {
kr_codeController.dispose();
_timer?.cancel();
super.onClose();
}
// 发送验证码
Future<void> kr_sendCode() async {
final account = KRAppRunData.getInstance().kr_account;
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;
// 发送验证码
final result = await _authApi.kr_sendCode(
type,
account,
KRAppRunData.getInstance().kr_areaCode, // 手机号不需要区号
2, // 删除账号的验证码类型
);
result.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
},
(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--;
} else {
timer.cancel();
kr_canSendCode.value = true;
}
});
},
);
}
// 请求删除账号
Future<void> requestDeleteAccount() async {
if (kr_codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.sendCode);
return;
}
final result = await _authApi.kr_deleteAccount(
KRAppRunData.getInstance().kr_loginType ?? KRLoginType.kr_telephone,
kr_codeController.text,
);
result.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
},
(success) {
KRCommonUtil.kr_showToast('删除账号成功');
KRAppRunData.getInstance().kr_loginOut();
},
);
// TODO: 实现删除账号的逻辑
}
}
@@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../controllers/kr_delete_account_controller.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
const KRDeleteAccountView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(
AppTranslations.kr_userInfo.myAccount,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () {
// 先收起键盘
FocusScope.of(context).unfocus();
// 返回到首页
Get.until((route) => route.isFirst);
},
),
),
body: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 20.w),
KrLocalImage(
imageName: 'delete_account',
width: 150.w,
height: 150.w,
imageType: ImageType.png,
),
SizedBox(height: 20.w),
Text(
'${AppTranslations.kr_userInfo.myAccount} ${KRAppRunData.getInstance().kr_account}\n${AppTranslations.kr_userInfo.willBeDeleted}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 20.w),
Text(
AppTranslations.kr_userInfo.deleteAccountWarning,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 20.w),
// 验证码输入框
Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: Theme.of(context).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: Icon(
Icons.lock_outline,
size: 20.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(width: 8.w),
Expanded(
child: TextField(
controller: controller.kr_codeController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'login.enterCode'.tr,
hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
if (controller.kr_codeHasText.value)
GestureDetector(
onTap: () {
controller.kr_codeController.clear();
},
child: Container(
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: Icon(
Icons.close,
size: 20.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
_buildSendCodeButton(context),
],
),
),
SizedBox(height: 20.w),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.requestDeleteAccount,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: EdgeInsets.symmetric(vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.w),
),
),
child: Text(
AppTranslations.kr_userInfo.requestDelete,
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
),
),
),
SizedBox(height: 20.w),
],
),
),
),
);
}
Widget _buildSendCodeButton(BuildContext context) {
return Obx(() => GestureDetector(
onTap: controller.kr_canSendCode.value ? controller.kr_sendCode : null,
child: Container(
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 0.5,
color: const Color(0xFFD2D2D2),
),
),
),
child: Center(
child: Text(
controller.kr_canSendCode.value
? AppTranslations.kr_login.sendCode
: '${controller.kr_countdown}s',
style: TextStyle(
fontSize: 14.sp,
color: controller.kr_canSendCode.value
? const Color(0xFF2196F3)
: Theme.of(context).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
fontWeight: FontWeight.w500,
),
),
),
),
));
}
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:get/get.dart';
import '../controllers/kr_home_controller.dart';
class KRHomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRHomeController>(
() => KRHomeController(),
);
Get.lazyPut<KRHomeController>(
() => KRHomeController(),
);
}
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
/// 首页基础视图状态枚举
enum KRHomeViewsStatus {
/// 未登录状态
kr_notLoggedIn,
/// 已登录状态
kr_loggedIn,
}
/// 首页列表视图状态枚举
enum KRHomeViewsListStatus {
kr_none,
kr_loading,
kr_error,
kr_serverList,
kr_countrySubscribeList,
kr_serverSubscribeList,
kr_subscribeList,
}
+203
View File
@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../localization/app_translations.dart';
import '../../../widgets/kr_app_text_style.dart';
import '../../../widgets/kr_loading_animation.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'kr_home_connection_info_view.dart';
import 'kr_home_connection_options_view.dart';
import 'kr_home_node_list_view.dart';
import '../widgets/kr_subscription_card.dart';
import 'kr_home_trial_card.dart';
import 'kr_home_last_day_card.dart';
import '../../../utils/kr_log_util.dart';
class KRHomeBottomPanel extends GetView<KRHomeController> {
const KRHomeBottomPanel({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
final currentStatus = controller.kr_currentListStatus.value;
KRLogUtil.kr_i('构建底部面板', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前视图状态: ${controller.kr_currentViewStatus.value}',
tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
if (controller.kr_currentListStatus.value ==
KRHomeViewsListStatus.kr_loading) {
return _kr_buildLoadingView();
}
if (controller.kr_currentListStatus.value ==
KRHomeViewsListStatus.kr_error) {
return _kr_buildErrorView(context);
}
if (currentStatus == KRHomeViewsListStatus.kr_serverList ||
currentStatus == KRHomeViewsListStatus.kr_countrySubscribeList ||
currentStatus == KRHomeViewsListStatus.kr_serverSubscribeList ||
currentStatus == KRHomeViewsListStatus.kr_subscribeList) {
return const KRHomeNodeListView();
}
return _kr_buildDefaultView(context);
});
}
Widget _kr_buildDefaultView(BuildContext context) {
// 使用 GetX 的 .obs 变量来避免重复访问
final hasValidSubscription =
controller.kr_subscribeService.kr_currentSubscribe.value != null;
final isTrial = controller.kr_subscribeService.kr_isTrial;
final isLastDay = controller.kr_subscribeService.kr_isLastDayOfSubscription;
final isNotLoggedIn = controller.kr_currentViewStatus.value ==
KRHomeViewsStatus.kr_notLoggedIn;
KRLogUtil.kr_i('构建默认视图', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否未登录: $isNotLoggedIn', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否有有效订阅: $hasValidSubscription', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否试用: ${isTrial.value}', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 主要内容区域
if (isNotLoggedIn)
// 未登录状态下,使用 SingleChildScrollView 让内容自然撑开
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
child: const KRHomeConnectionOptionsView(),
),
],
),
)
else
// 已登录状态下,使用固定高度
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 1. 如果已订阅,展示当前连接卡片
if (hasValidSubscription)
Container(
margin: EdgeInsets.only(top: 12.h),
child: const KRHomeConnectionInfoView())
else
Container(
margin:
EdgeInsets.only(top: 12.h, left: 12.w, right: 12.w),
child: const KRSubscriptionCard()),
// 2. 如果已订阅且是试用,展示试用卡片
if (hasValidSubscription && isTrial.value)
Container(
margin: EdgeInsets.only(top: 12.h),
child: const KRHomeTrialCard(),
),
// 3. 如果已订阅且是最后一天,展示最后一天卡片
if (hasValidSubscription && isLastDay.value && !isTrial.value)
Container(
margin: EdgeInsets.only(top: 12.h),
child: const KRHomeLastDayCard(),
),
// 4. 连接选项(分组和国家入口)
Padding(
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
child: const KRHomeConnectionOptionsView(),
),
],
),
),
),
],
);
}
Widget _kr_buildLoadingView() {
KRLogUtil.kr_i('构建加载视图', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
return Center(
child: CircularProgressIndicator(
color: Colors.green,
strokeWidth: 2.0,
),
);
}
Widget _kr_buildErrorView(BuildContext context) {
return Container(
height: 200.w,
padding: EdgeInsets.all(16.w),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48.w,
color: Theme.of(context).colorScheme.error,
),
SizedBox(height: 16.w),
Text(
AppTranslations.kr_home.error,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 8.w),
Text(
AppTranslations.kr_home.checkNetwork,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 24.w),
SizedBox(
width: 200.w,
height: 44.h,
child: ElevatedButton(
onPressed: () => controller.kr_refreshAll(),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
AppTranslations.kr_home.retry,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../widgets/kr_simple_loading.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
class KRHomeConnectionInfoView extends GetView<KRHomeController> {
const KRHomeConnectionInfoView({super.key});
@override
Widget build(BuildContext context) {
return _buildConnectCard(context);
}
/// 当前连接
Widget _buildConnectCard(BuildContext context) {
return Obx(() {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
width: double.infinity,
height: 116.w,
decoration: ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.w),
),
),
child: Padding(
padding: EdgeInsets.all(14.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.currentConnectionTitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
// 切换节点按钮
GestureDetector(
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_subscribeList);
},
child: Row(
children: [
Text(
AppTranslations.kr_home.switchNode,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
],
),
SizedBox(height: 10.w),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
KRCountryFlag(
countryCode: controller.kr_getCurrentNodeCountry(),
),
SizedBox(width: 10.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_currentNodeName.value,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 6.w),
Row(
children: [
Obx(() {
final delay = controller.kr_currentNodeLatency.value;
// 获取延迟颜色
Color getLatencyColor(int delay) {
if (delay == -2) {
return Colors.green;
} else if (delay == -1) {
return Theme.of(context).primaryColor;
} else if (delay < 500) {
return Colors.green;
} else if (delay < 3000) {
return Color(0xFFFFB700);
} else {
return Colors.red;
}
}
// 获取延迟文本
String getLatencyText(int delay) {
if (delay == -2) {
return '0ms';
} else if (delay == -1) {
return AppTranslations.kr_home.connecting;
} else if (delay >= 3000) {
return AppTranslations.kr_home.timeout;
} else {
return '${delay}ms';
}
}
if (delay == -1) {
return Row(
children: [
KRSimpleLoading(
color: Colors.green,
size: 12.w,
duration: const Duration(milliseconds: 800),
),
SizedBox(width: 2.w),
Text(
'connecting',
style: TextStyle(
color: Colors.green,
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
);
}
return Row(
children: [
Icon(Icons.signal_cellular_alt,
size: 12.w,
color: getLatencyColor(delay)),
SizedBox(width: 2),
Text(
getLatencyText(delay),
style: KrAppTextStyle(
color: getLatencyColor(delay),
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
);
}),
// 只在非连接中状态显示上下行
Obx(() {
final delay = controller.kr_currentNodeLatency.value;
if (delay == -1) {
return const SizedBox.shrink();
}
return Row(
children: [
SizedBox(width: 10.w),
Icon(Icons.arrow_upward,
size: 12.w,
color: Theme.of(context).textTheme.bodySmall?.color),
Text(
controller.kr_formatBytes(KRSingBoxImp.instance.kr_stats.value.uplink),
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
SizedBox(width: 10.w),
Icon(Icons.arrow_downward,
size: 12.w,
color: Theme.of(context).textTheme.bodySmall?.color),
Text(
controller.kr_formatBytes(KRSingBoxImp.instance.kr_stats.value.downlink),
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
);
}),
],
),
],
),
],
),
CupertinoSwitch(
value: controller.kr_isConnected.value,
onChanged: (bool value) {
controller.kr_toggleSwitch(value);
},
activeTrackColor: Colors.blue,
),
],
),
],
),
),
);
});
}
}
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
class KRHomeConnectionOptionsView extends GetView<KRHomeController> {
const KRHomeConnectionOptionsView({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppTranslations.kr_home.connectionSectionTitle,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
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);
},
),
),
],
),
],
);
}
Widget _buildConnectionOption(String icon, String label, BuildContext context,
{VoidCallback? onTap}) {
return GestureDetector(
onTap: () {
if (controller.kr_subscribeService.kr_currentSubscribe.value == null) {
// 未订阅状态下跳转到购买会员页面
Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP);
} else {
// 已订阅状态下执行原有的点击事件
onTap?.call();
}
},
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
KrLocalImage(
imageName: icon,
width: 32.w,
height: 32.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
SizedBox(height: 12.w),
Row(
children: [
Expanded(
child: Text(
label,
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
],
),
),
);
}
}
+149
View File
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../routes/app_pages.dart';
import '../controllers/kr_home_controller.dart';
/// 最后一天卡片组件
class KRHomeLastDayCard extends GetView<KRHomeController> {
const KRHomeLastDayCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
width: double.infinity,
decoration: ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.w),
),
),
child: Padding(
padding: EdgeInsets.all(14.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 顶部标题和订阅按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.lastDaySubscriptionStatus,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
GestureDetector(
onTap: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
child: Row(
children: [
Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Colors.blue,
),
],
),
),
],
),
// 倒计时显示
SizedBox(height: 10.w),
Row(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
color: Colors.blue,
size: 16.w,
),
SizedBox(width: 4.w),
Text(
AppTranslations.kr_home.lastDaySubscriptionMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() {
final isLastDay =
controller.kr_subscribeService.kr_isLastDayOfSubscription.value;
final remainingTime = controller.kr_subscribeService.kr_subscriptionRemainingTime.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
remainingTime,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isLastDay
? (DateTime.now().millisecondsSinceEpoch %
2000 <
1000
? Colors.red
: Colors.blue)
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
SizedBox(height: 4.w),
Text(
AppTranslations.kr_home.subscriptionEndMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
),
],
);
}),
],
),
),
],
),
],
),
),
);
}
}
+912
View File
@@ -0,0 +1,912 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../../../model/business/kr_outbound_item.dart';
import '../../../utils/kr_log_util.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/widgets/kr_network_image.dart';
import '../../../widgets/kr_simple_loading.dart';
import '../../../../singbox/model/singbox_proxy_type.dart';
/// 节点列表视图组件
/// 用于展示所有节点相关的列表视图
class KRHomeNodeListView extends GetView<KRHomeController> {
const KRHomeNodeListView({super.key});
// 添加常量定义
static const Color krModernGreen = Color(0xFF4CAF50);
static const Color krModernGreenLight = Color(0xFF81C784);
// 存储每个节点的随机延迟值(仅用于界面显示)
static final Map<String, int> _fakeDelays = {};
/// 获取显示的延迟值
int _getDisplayDelay(KRHomeController controller, KROutboundItem item) {
// 如果已连接,使用真实的延迟值
if (controller.kr_isConnected.value) {
return item.urlTestDelay.value;
}
// 如果未连接,使用随机延迟值
if (!_fakeDelays.containsKey(item.tag)) {
// 生成30ms-100ms之间的随机延迟
final random = Random();
_fakeDelays[item.tag] = 30 + random.nextInt(71); // 30 + (0-70) = 30-100ms
}
return _fakeDelays[item.tag] ?? 0;
}
@override
Widget build(BuildContext context) {
return Obx(() {
// 根据列表状态选择不同的视图
switch (controller.kr_currentListStatus.value) {
case KRHomeViewsListStatus.kr_serverList:
return _buildServerList(context);
case KRHomeViewsListStatus.kr_subscribeList:
return _buildSubscribeList(context);
case KRHomeViewsListStatus.kr_countrySubscribeList:
return _kr_buildRegionList(context);
case KRHomeViewsListStatus.kr_serverSubscribeList:
return _kr_buildServerSubscribeList(context);
default:
return const SizedBox.shrink();
}
});
}
/// 服务器列表视图
/// 构建专用服务器列表
Widget _buildServerList(BuildContext context) {
return Container(
width: ScreenUtil().screenWidth,
height: 360.w, // 减小高度比例
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.w),
topRight: Radius.circular(20.w),
),
),
child: Column(
children: [
// 标题栏
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.serverListTitle,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
GestureDetector(
onTap: () {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Icon(
Icons.close,
size: 24.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
// 列表内容
Expanded(
child: Obx(() {
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noServers,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w),
itemCount: controller.kr_subscribeService.groupOutboundList.length,
itemBuilder: (context, index) {
final group =
controller.kr_subscribeService.groupOutboundList[index];
return Container(
margin: EdgeInsets.only(bottom: 8.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.1),
width: 1.w,
),
),
child: InkWell(
onTap: () {
controller.kr_setCurrentGroup(group);
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_serverSubscribeList;
},
borderRadius: BorderRadius.circular(12.w),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
KRNetworkImage(
kr_imageUrl: group.icon,
kr_width: 32.w,
kr_height: 32.w,
kr_fit: BoxFit.cover,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
group.tag,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
),
);
},
);
}),
),
],
),
);
}
/// 国家订阅列表视图
Widget _kr_buildRegionList(BuildContext context) {
return _kr_buildListPage(
context,
title: AppTranslations.kr_home.countryListTitle,
listContent: Obx(() {
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noRegions,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return _kr_buildListContainer(
context,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 0),
itemCount:
controller.kr_subscribeService.countryOutboundList.length,
itemBuilder: (context, index) {
final country =
controller.kr_subscribeService.countryOutboundList[index];
return Column(
children: [
// 主区域
InkWell(
onTap: () {
country.isExpand.value = !country.isExpand.value;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.w),
child: Row(
children: [
KRCountryFlag(
countryCode: country.country,
width: 40.w,
height: 40.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller
.kr_getCountryFullName(country.country),
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
],
),
),
Obx(() {
return Icon(
country.isExpand.value
? Icons.keyboard_arrow_down
: Icons.arrow_forward_ios,
size: 16.w,
color:
Theme.of(context).textTheme.bodySmall?.color,
);
}),
],
),
),
),
// 展开的服务器列表
Obx(() {
final isExpanded = country.isExpand.value;
if (!isExpanded) return const SizedBox();
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.w),
itemCount: country.outboundList.length,
itemBuilder: (context, index) {
final server = country.outboundList[index];
return Column(
children: [
InkWell(
onTap: () {
print(server.tag);
KRSingBoxImp.instance
.kr_selectOutbound(server.tag);
controller.kr_selectNode(server.tag);
// 添加状态切换,回到默认状态
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.w,
horizontal: 16.w,
),
decoration: BoxDecoration(
// 添加轻微的背景色以区分点击区域
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8.w),
),
child: _kr_buildNodeListItem(
context,
item: server,
),
),
),
// 添加分隔线
if (index < country.outboundList.length - 1)
Divider(
height: 1.w,
indent: 16.w,
endIndent: 16.w,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
],
);
},
);
}),
Divider(
height: 1.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
);
},
),
);
}),
);
}
/// 服务器订阅列表视图
// 修改服务器订阅列表视图
Widget _kr_buildServerSubscribeList(BuildContext context) {
return _kr_buildListPage(
context,
title: controller.kr_currentGroup.value?.tag ?? '',
onBack: () => controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_serverList,
listContent: Obx(() {
final servers = controller.kr_currentGroup.value?.outboundList ?? [];
if (servers.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noNodes,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return _kr_buildListContainer(
context,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(16.w, 16.w, 16.w, 0),
itemCount: servers.length,
itemBuilder: (context, index) {
final server = servers[index];
return Column(
children: [
InkWell(
onTap: () {
KRLogUtil.kr_i(server.tag);
KRSingBoxImp.instance.kr_selectOutbound(server.tag);
controller.kr_selectNode(server.tag);
// 添加状态切换,回到默认状态
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4.w),
child: _kr_buildNodeListItem(
context,
item: server,
),
),
),
if (index < servers.length - 1)
Divider(
height: 1.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
);
},
),
);
}),
);
}
Widget _kr_buildListPage(
BuildContext context, {
required String title,
VoidCallback? onBack,
required Widget listContent,
}) {
return Container(
width: ScreenUtil().screenWidth,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.w),
topRight: Radius.circular(20.w),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_kr_buildTitleBar(
context,
title: title,
onBack: onBack,
onClose: () =>
controller.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none,
),
SizedBox(height: 16.w),
Expanded(
child: Column(
children: [
Expanded(child: listContent),
// 添加底部间距
SizedBox(height: 12.w),
],
),
),
],
),
);
}
// 抽取公共的标题栏组件
Widget _kr_buildTitleBar(
BuildContext context, {
required String title,
VoidCallback? onBack,
VoidCallback? onClose,
}) {
return Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
if (onBack != null) ...[
GestureDetector(
onTap: onBack,
child: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(width: 8.w),
],
Text(
title,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
if (onClose != null)
GestureDetector(
onTap: onClose,
child: Icon(
Icons.close,
size: 24.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
/// 构建列表容器
Widget _kr_buildListContainer(
BuildContext context, {
required Widget child,
}) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: child,
);
}
/// 构建节点列表项
Widget _kr_buildNodeListItem(
BuildContext context, {
required KROutboundItem item,
}) {
// 获取延迟颜色
Color getLatencyColor(int delay) {
if (delay == 0) {
return Colors.transparent;
} else if (delay < 500) {
return krModernGreen;
} else if (delay < 3000) {
return Color(0xFFFFB700); // 使用更容易看清的黄色
} else {
return Colors.red;
}
}
// 获取图标颜色
Color? getIconColor(int delay) {
if (delay == 0) {
return null;
} else if (delay >= 3000) {
return Colors.red;
}
return null;
}
return Container(
key: ValueKey(item.id),
padding: EdgeInsets.symmetric(vertical: 8.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: "home_list_location",
width: 36.w,
height: 36.w,
color: getIconColor(item.urlTestDelay.value),
),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
item.tag,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Obx(
() => controller.kr_cutTag.value == item.tag
? Container(
margin: EdgeInsets.only(left: 4.w),
padding: EdgeInsets.symmetric(
horizontal: 4.w, vertical: 1.w),
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
),
)
: const SizedBox.shrink(),
),
],
),
SizedBox(height: 2.w),
Text(
item.city,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
// 显示延迟速度
GetBuilder<KRHomeController>(
id: item.tag,
builder: (controller) {
// 获取显示的延迟值
int displayDelay = _getDisplayDelay(controller, item);
return Container(
alignment: Alignment.center,
child: Text(
displayDelay == 0
? ''
: displayDelay >= 3000
? AppTranslations.kr_home.timeout
: '${displayDelay}ms',
style: KrAppTextStyle(
fontSize: 12,
color: getLatencyColor(displayDelay),
),
),
);
},
),
],
),
);
}
// 修改订阅列表视图
Widget _buildSubscribeList(BuildContext context) {
return _kr_buildListPage(
context,
title: AppTranslations.kr_home.nodeListTitle,
listContent: Obx(() {
if (controller.kr_subscribeService.allList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noNodes,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
// 自动触发延迟测试(仅在未连接状态下)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value) {
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'NodeListView');
controller.kr_urlTest();
}
});
return _kr_buildListContainer(
context,
child: ListView(
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0),
children: [
// 延迟测试按钮作为第一个列表项
InkWell(
onTap: () => controller.kr_urlTest(),
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.w),
margin: EdgeInsets.only(top: 8.w), // 添加上方间距
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 36.w,
height: 36.w,
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: controller.kr_isLatency.value
? KRSimpleLoading(
color: krModernGreen,
size: 24.w,
duration: const Duration(milliseconds: 800),
)
: Icon(
Icons.speed,
size: 24.w,
color: krModernGreen,
),
),
),
SizedBox(width: 8.w),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_isLatency.value
? AppTranslations.kr_home.testing
: AppTranslations.kr_home.testLatency,
style: KrAppTextStyle(
fontSize: 14,
color: controller.kr_isLatency.value
? Theme.of(context)
.textTheme
.bodySmall
?.color
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
fontWeight: controller.kr_isLatency.value
? FontWeight.normal
: FontWeight.w500,
),
),
if (!controller.kr_isLatency.value) ...[
SizedBox(height: 2.w),
Text(
AppTranslations.kr_home.refreshLatencyDesc,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
),
],
],
),
),
if (!controller.kr_isLatency.value)
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
),
// 分隔线
Divider(
height: 16.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// Auto 选项
InkWell(
onTap: () {
controller.kr_selectNode('auto');
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: "home_list_location",
width: 36.w,
height: 36.w,
color: controller.kr_cutTag.value == 'auto'
? Colors.green
: null,
),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
AppTranslations.kr_home.autoSelect,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (controller.kr_cutTag.value == 'auto')
Container(
margin: EdgeInsets.only(left: 4.w),
padding: EdgeInsets.symmetric(
horizontal: 4.w, vertical: 1.w),
decoration: BoxDecoration(
color:
krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
),
),
],
),
SizedBox(height: 2.w),
Obx(() {
// 获取当前自动选择的节点
String selectedNode =
AppTranslations.kr_home.autoSelect;
int delay = 0;
for (var group
in KRSingBoxImp.instance.kr_activeGroups) {
if (group.type == ProxyType.urltest) {
selectedNode = group.selected;
delay = controller
.kr_subscribeService.keyList[group.selected]
?.urlTestDelay.value ??
0;
break;
}
}
return Text(
selectedNode,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
);
}),
],
),
),
Obx(() {
// 获取当前自动选择的节点
String selectedNode =
AppTranslations.kr_home.autoSelect;
int delay = 0;
for (var group
in KRSingBoxImp.instance.kr_activeGroups) {
if (group.type == ProxyType.urltest) {
selectedNode = group.selected;
delay = controller
.kr_subscribeService
.keyList[group.selected]
?.urlTestDelay
.value ??
0;
break;
}
}
return delay > 0
? Container(
alignment: Alignment.center,
child: Text(
delay < 3000
? '${delay}ms'
: AppTranslations.kr_home.timeout,
style: KrAppTextStyle(
fontSize: 12,
color: delay < 3000
? Colors.green
: Colors.red,
),
),
)
: const SizedBox.shrink();
}),
],
),
),
),
// 分隔线
Divider(
height: 16.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// 节点列表
...controller.kr_subscribeService.allList
.map((node) => Column(
children: [
InkWell(
onTap: () {
KRLogUtil.kr_i(node.tag);
KRSingBoxImp.instance.kr_selectOutbound(node.tag);
controller.kr_selectNode(node.tag);
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4.w),
child: _kr_buildNodeListItem(
context,
item: node,
),
),
),
if (node !=
controller.kr_subscribeService.allList.last)
Divider(
height: 1.w,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
],
))
.toList(),
],
),
);
}),
);
}
}
@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:math';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
class KRHomeSubscriptionView extends GetView<KRHomeController> {
const KRHomeSubscriptionView({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
if (!KRAppRunData().kr_isLogin.value) {
return const SizedBox.shrink();
}
final currentSubscribe =
controller.kr_subscribeService.kr_currentSubscribe.value;
if (currentSubscribe == null) {
return const SizedBox.shrink();
}
KRLogUtil.kr_i('当前订阅名称: ${currentSubscribe.name}',
tag: 'SubscriptionView');
final totalTraffic = currentSubscribe.traffic;
final usedTraffic = currentSubscribe.download + currentSubscribe.upload;
final hasTrafficLimit = totalTraffic > 0;
var trafficPercentage =
hasTrafficLimit ? (usedTraffic / totalTraffic).clamp(0.0, 1.0) : 0.0;
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bolt_rounded,
size: 14.w,
color: Theme.of(context).brightness == Brightness.light
? Colors.black54
: Colors.white54,
),
SizedBox(width: 4.w),
Expanded(
child: Text(
currentSubscribe.name,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 4.w),
Container(
height: 3.h,
width: 20.w,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[200]
: Colors.grey[800],
borderRadius: BorderRadius.circular(1.5.r),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: trafficPercentage,
child: Container(
decoration: BoxDecoration(
color: _getTrafficColor(trafficPercentage),
borderRadius: BorderRadius.circular(1.5.r),
),
),
),
),
SizedBox(width: 4.w),
Icon(
Icons.swap_horiz,
size: 14.w,
color: Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(0.5)
: Colors.white.withOpacity(0.5),
),
],
),
);
});
}
Widget _buildLoadingView(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.grey[900]!,
Theme.of(context).brightness == Brightness.light
? Colors.grey[50]!
: Colors.grey[800]!,
],
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
const SizedBox(width: 16),
Text(
AppTranslations.kr_home.loading,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
IconData _getStatusIcon(KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return Icons.error_outline_rounded;
} else if (difference.inDays <= 1) {
return Icons.warning_amber_rounded;
} else {
return Icons.check_circle_outline_rounded;
}
}
Color _getStatusColor(
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return Colors.red;
} else if (difference.inDays <= 1) {
return Colors.orange;
} else {
return const Color(0xFF00E52B);
}
}
String _getStatusText(KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return '已过期';
} else if (difference.inDays <= 1) {
return '即将到期';
} else {
return '有效';
}
}
Color _getTrafficColor(double percentage) {
if (percentage >= 0.9) {
return Colors.red;
} else if (percentage >= 0.7) {
return Colors.orange;
} else {
return const Color(0xFF00E52B);
}
}
String _formatTraffic(int bytes) {
if (bytes < 1024) {
return '$bytes B';
} else if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
} catch (e) {
return dateStr;
}
}
}
+148
View File
@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../routes/app_pages.dart';
import '../../../services/kr_subscribe_service.dart';
import '../../../utils/kr_log_util.dart';
import '../controllers/kr_home_controller.dart';
/// 试用卡片组件
class KRHomeTrialCard extends GetView<KRHomeController> {
const KRHomeTrialCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
width: double.infinity,
decoration: ShapeDecoration(
color: Theme.of(context).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.w),
),
),
child: Padding(
padding: EdgeInsets.all(14.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 顶部标题和订阅按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.trialStatus,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
GestureDetector(
onTap: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
child: Row(
children: [
Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Colors.blue,
),
],
),
),
],
),
// 倒计时显示
SizedBox(height: 10.w),
Row(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
color: Colors.blue,
size: 16.w,
),
SizedBox(width: 4.w),
Text(
AppTranslations.kr_home.trialing,
style: KrAppTextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCountdown(),
],
),
),
],
),
],
),
),
);
}
Widget _buildCountdown() {
return Obx(() {
final subscribeService = KRSubscribeService();
final remainingTime = subscribeService.kr_trialRemainingTime.value;
final isExpired = remainingTime.isEmpty;
return Builder(
builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
remainingTime,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isExpired
? (DateTime.now().millisecondsSinceEpoch % 2000 < 1000
? Colors.red
: Colors.blue)
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 4.w),
Text(
AppTranslations.kr_home.trialEndMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
});
}
}
+326
View File
@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/modules/kr_login/views/kr_login_view.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import '../../../services/kr_subscribe_service.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import '../widgets/kr_home_map_view.dart';
import '../widgets/kr_subscribe_selector_view.dart';
import 'kr_home_bottom_panel.dart';
import 'kr_home_subscription_view.dart';
// 定义新的绿色
const Color krModernGreen = Color(0xFF00E52B);
const Color krModernGreenLight = Color(0xFF66FF85);
const Color krModernGreenDark = Color(0xFF00B322);
class KRHomeView extends GetView<KRHomeController> {
const KRHomeView({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.kr_currentViewStatus.value ==
KRHomeViewsStatus.kr_notLoggedIn) {
return Scaffold(
body: Stack(
children: [
// 地图视图
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),
),
],
),
child: const KRLoginView(),
),
),
],
),
);
}
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
body: Stack(
children: [
// 地图视图
const KRHomeMapView(),
// 顶部工具栏
Positioned(
top: MediaQuery.of(context).padding.top + 16,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 左侧状态组
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).cardColor
: Theme.of(context).cardColor.withOpacity(0.8),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: krModernGreen,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Obx(() {
return Text(
controller.kr_connectText.value,
style: TextStyle(
color: Theme.of(context).brightness ==
Brightness.light
? Colors.black
: Colors.white,
fontSize: 12,
),
);
}),
],
),
),
// 订阅视图
Obx(() {
if (!KRAppRunData().kr_isLogin.value) {
return const SizedBox.shrink();
}
final currentSubscribe = controller
.kr_subscribeService.kr_currentSubscribe.value;
if (currentSubscribe == null) {
return const SizedBox.shrink();
}
return Expanded(
child: GestureDetector(
onTap: () {
if (KRSubscribeService()
.kr_currentStatus
.value ==
KRSubscribeServiceStatus.kr_loading) {
return;
}
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
child: KRSubscribeSelectorView(),
),
);
},
child: Container(
margin:
const EdgeInsets.only(left: 12, right: 12),
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).cardColor
: Theme.of(context)
.cardColor
.withOpacity(0.8),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const KRHomeSubscriptionView(),
),
),
);
}),
],
),
),
// 右侧按钮组
Row(
mainAxisSize: MainAxisSize.min,
children: [
// 消息按钮
Obx(() {
if (!KRAppRunData().kr_isLogin.value) {
return const SizedBox.shrink();
}
return Container(
width: 32,
height: 32,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).cardColor
: Theme.of(context).cardColor.withOpacity(0.8),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
onPressed: () {
Get.toNamed(Routes.KR_MESSAGE);
},
icon: Icon(
Icons.notifications_outlined,
color: Theme.of(context).brightness ==
Brightness.light
? Colors.blue
: Theme.of(context)
.textTheme
.bodyMedium
?.color
?.withOpacity(0.8),
size: 16,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
);
}),
// 刷新按钮
Obx(() {
if (!KRAppRunData().kr_isLogin.value) {
return const SizedBox.shrink();
}
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).cardColor
: Theme.of(context).cardColor.withOpacity(0.8),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
onPressed: () {
controller.kr_refreshAll();
},
icon: Icon(
Icons.refresh,
color: Theme.of(context).brightness ==
Brightness.light
? Colors.blue
: Theme.of(context)
.textTheme
.bodyMedium
?.color
?.withOpacity(0.8),
size: 16,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
);
}),
],
),
],
),
),
// 底部面板
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Obx(() {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: controller.kr_bottomPanelHeight.value.w,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
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 KRHomeBottomPanel(),
);
}),
),
],
),
);
});
}
Color _getTrafficColor(double percentage) {
if (percentage >= 0.9) {
return Colors.red;
} else if (percentage >= 0.7) {
return Colors.orange;
} else {
return krModernGreen;
}
}
}
+126
View File
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import '../../../utils/kr_fm_tc.dart';
import '../controllers/kr_home_controller.dart';
import '../../../widgets/kr_local_image.dart';
import '../../../utils/kr_log_util.dart';
/// 首页地图视图组件
class KRHomeMapView extends GetView<KRHomeController> {
const KRHomeMapView({super.key});
@override
Widget build(BuildContext context) {
// 初始化地图缓存
// KRFMTC.kr_initMapCache();
return Obx(() => FlutterMap(
mapController: controller.kr_mapController,
options: MapOptions(
initialCenter: _kr_getInitialMapCenter(),
initialZoom: 4.0,
initialRotation: 0,
backgroundColor: Theme.of(context).primaryColor,
onMapEvent: (event) {
try {
if (event is MapEventMoveEnd) {
if (event.source == MapEventSource.dragEnd ||
event.source == MapEventSource.multiFingerEnd) {
controller.kr_isUserMoving.value = true;
}
}
} catch (e) {
KRLogUtil.kr_e('地图事件处理失败: $e', tag: 'HomeMapView');
}
},
keepAlive: true,
interactionOptions: InteractionOptions(
enableMultiFingerGestureRace: true,
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
_kr_buildTileLayer(context),
MarkerLayer(
markers: controller.kr_subscribeService.allList
.map((item) => _buildStyledMarker(item))
.toList(),
),
],
));
}
/// 构建地图瓦片层
Widget _kr_buildTileLayer(BuildContext context) {
return TileLayer(
urlTemplate: KRFMTC.kr_getTileUrl(),
subdomains: KRFMTC.kr_getTileSubdomains(),
userAgentPackageName: 'app.brAccelerator.com',
tileProvider: KRFMTC.kr_getTileProvider(),
tileBuilder: (context, child, tileImage) {
return child;
},
);
}
/// 构建样式化的标记
Marker _buildStyledMarker(dynamic node) {
return Marker(
point: LatLng(node.latitude, node.longitude),
width: 42.w,
height: 34.w,
child: GetBuilder<KRHomeController>(
id: node.tag,
builder: (controller) {
// 确定颜色
Color? markerColor;
if (node.urlTestDelay.value == 0) {
// 延迟为0时使用默认颜色
markerColor = null;
} else if (node.urlTestDelay.value < 500) {
// 延迟小于500ms显示绿色
markerColor = const Color(0xFF00E52B).withOpacity(0.7);
} else if (node.urlTestDelay.value < 3000) {
// 延迟小于3000ms显示黄色
markerColor = Colors.yellow;
} else {
// 超时显示红色
markerColor = Colors.red;
}
return KrLocalImage(
imageName: "location",
width: 42.w,
height: 34.w,
color: markerColor,
);
},
),
);
}
/// 获取初始地图中心点
LatLng _kr_getInitialMapCenter() {
if (controller.kr_isUserMoving.value) {
return controller.kr_lastMapCenter.value;
}
if (controller.kr_cutSeletedTag.isEmpty) {
return const LatLng(35.0, 105.0); // 修改默认位置为中国中部
}
final selectedNode = controller.kr_subscribeService.allList
.firstWhereOrNull((item) => item.tag == controller.kr_cutSeletedTag.value);
if (selectedNode == null) {
return const LatLng(35.0, 105.0); // 修改默认位置为中国中部
}
// 更新最后的地图中心点
controller.kr_lastMapCenter.value =
LatLng(selectedNode.latitude, selectedNode.longitude);
return controller.kr_lastMapCenter.value;
}
}
@@ -0,0 +1,275 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
class KRSubscribeSelectorView extends StatelessWidget {
final KRHomeController? controller;
const KRSubscribeSelectorView({
super.key,
this.controller,
});
@override
Widget build(BuildContext context) {
final homeController = controller ?? Get.find<KRHomeController>();
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
width: MediaQuery.of(context).size.width * 0.85,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10.r,
offset: Offset(0, 2.w),
spreadRadius: 0,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.05),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_purchaseMembership.selectPackage,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.titleLarge?.color,
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(20.r),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.close_rounded,
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.6),
size: 18.w,
),
),
),
),
],
),
),
Obx(() {
final subscribes = homeController.kr_subscribeService.kr_availableSubscribes;
if (subscribes.isEmpty) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16.w, horizontal: 12.w),
child: Column(
children: [
Icon(
Icons.subscriptions_outlined,
size: 48.w,
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.3),
),
SizedBox(height: 12.w),
Text(
AppTranslations.kr_purchaseMembership.noData,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.5),
),
),
],
),
);
}
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.5,
),
child: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.symmetric(vertical: 4.w, horizontal: 4.w),
itemCount: subscribes.length,
itemBuilder: (context, index) {
final subscribe = subscribes[index];
final isCurrent = subscribe.id == homeController.kr_subscribeService.kr_currentSubscribe.value?.id;
return _SubscribeItem(
subscribe: subscribe,
isCurrent: isCurrent,
onTap: () {
homeController.kr_switchSubscribe(subscribe);
Get.back();
},
);
},
),
);
}),
SizedBox(height: 8.w),
],
),
);
}
}
class _SubscribeItem extends StatelessWidget {
final KRUserAvailableSubscribeItem subscribe;
final bool isCurrent;
final VoidCallback onTap;
const _SubscribeItem({
required this.subscribe,
required this.isCurrent,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final usedTraffic = (subscribe.download + subscribe.upload) / 1024 / 1024 / 1024;
final totalTraffic = subscribe.traffic / 1024 / 1024 / 1024;
var percentage = totalTraffic > 0 ? usedTraffic / totalTraffic : 0.0;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final isUnlimited = subscribe.traffic == 0;
String getUsedTrafficDisplay() {
if (usedTraffic < 1) {
return '${(usedTraffic * 1024).toStringAsFixed(2)}MB';
} else {
return '${usedTraffic.toStringAsFixed(2)}GB';
}
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.w),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.r),
child: Ink(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isCurrent
? Colors.blue.withOpacity(isDarkMode ? 0.15 : 0.08)
: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isCurrent
? Colors.blue.withOpacity(isDarkMode ? 0.5 : 0.3)
: Theme.of(context).dividerColor.withOpacity(0.3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
subscribe.name,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.titleLarge?.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (isCurrent)
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.w),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.2),
blurRadius: 6.w,
offset: Offset(0, 1.w),
),
],
),
child: Text(
AppTranslations.kr_home.currentConnectionTitle,
style: KrAppTextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 8.w),
Text(
isUnlimited
? AppTranslations.kr_purchaseMembership.unlimitedTraffic
: '${getUsedTrafficDisplay()} / ${totalTraffic.toStringAsFixed(2)}GB',
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8),
),
),
if (!isUnlimited) ...[
SizedBox(height: 8.w),
ClipRRect(
borderRadius: BorderRadius.circular(4.r),
child: LinearProgressIndicator(
value: percentage.clamp(0.0, 1.0),
backgroundColor: isDarkMode
? Colors.grey[700]?.withOpacity(0.7)
: Colors.grey[300]?.withOpacity(0.9),
valueColor: AlwaysStoppedAnimation<Color>(
_getTrafficColor(percentage, isDarkMode),
),
minHeight: 4.w,
),
),
],
],
),
),
),
),
);
}
Color _getTrafficColor(double percentage, bool isDarkMode) {
if (percentage >= 0.9) {
return isDarkMode
? Colors.red.withOpacity(0.8)
: Colors.red.withOpacity(0.7);
} else if (percentage >= 0.7) {
return isDarkMode
? Colors.orange.withOpacity(0.8)
: Colors.orange.withOpacity(0.7);
} else {
return isDarkMode
? Colors.blue.withOpacity(0.8)
: Colors.blue.withOpacity(0.7);
}
}
}
+109
View File
@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import '../../../widgets/kr_app_text_style.dart';
/// 订阅卡片组件
class KRSubscriptionCard extends StatelessWidget {
const KRSubscriptionCard({
super.key,
});
@override
Widget build(BuildContext context) {
return _kr_buildSubscriptionCard(context);
}
// 构建订阅卡片
Widget _kr_buildSubscriptionCard(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 44.w,
height: 44.w,
margin: EdgeInsets.only(top: 16.h),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.language,
color: Colors.blue,
size: 26.w,
),
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
AppTranslations.kr_home.subscriptionDescription,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
SizedBox(height: 16.h),
Padding(
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
child: SizedBox(
width: double.infinity,
height: 42.h,
child: ElevatedButton(
onPressed: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
),
],
),
);
}
Widget _kr_buildListContainer(
BuildContext context, {
required Widget child,
EdgeInsetsGeometry? margin,
bool addBottomPadding = true,
}) {
return Container(
margin: margin ?? EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: IntrinsicWidth(
child: child,
),
);
}
}
+12
View File
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_invite_controller.dart';
class KRInviteBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRInviteController>(
() => KRInviteController(),
);
}
}
@@ -0,0 +1,306 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.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/routes/app_pages.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/modules/kr_main/controllers/kr_main_controller.dart';
import 'package:easy_refresh/easy_refresh.dart';
/// 邀请进度状态
class KRInviteProgress {
final int pending;
final int processing;
final int success;
final int expired;
final int registers;
final int totalCommission;
KRInviteProgress({
this.pending = 0,
this.processing = 0,
this.success = 0,
this.expired = 0,
this.registers = 0,
this.totalCommission = 0,
});
}
class KRInviteController extends GetxController {
final kr_progress = KRInviteProgress(
pending: 0,
processing: 0,
success: 0,
expired: 0,
registers: 0,
totalCommission: 0,
).obs;
final kr_referCode = ''.obs;
final kr_isLoading = false.obs;
final count = 0.obs;
final EasyRefreshController refreshController = EasyRefreshController();
@override
void onInit() {
super.onInit();
ever(KRAppRunData.getInstance().kr_isLogin, (value) {
if (value) {
_kr_fetchUserInfo();
_kr_fetchAffiliateCount();
} else {
kr_progress.value = KRInviteProgress(
pending: 0,
processing: 0,
success: 0,
expired: 0,
registers: 0,
totalCommission: 0,
);
kr_referCode.value = '';
}
});
if (KRAppRunData.getInstance().kr_isLogin.value) {
_kr_fetchUserInfo();
_kr_fetchAffiliateCount();
}
}
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;
},
);
} catch (e) {
KRCommonUtil.kr_showToast(e.toString());
} finally {
kr_isLoading.value = false;
}
}
Future<bool> kr_checkLoginStatus() async {
return KRAppRunData.getInstance().kr_isLogin.value;
}
/// 获取分享链接
String kr_getShareLink() {
if (kr_referCode.isEmpty) {
return '';
}
return '${AppConfig.getInstance().kr_invitation_link}${kr_referCode.value}';
}
/// 获取二维码内容
String kr_getQRCodeContent() {
return kr_getShareLink();
}
/// 复制文本到剪贴板
Future<void> _kr_copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text));
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.copiedToClipboard);
}
Future<void> kr_handleQRShare() async {
if (!await kr_checkLoginStatus()) {
Get.find<KRMainController>().kr_setPage(0);
return;
}
if (kr_referCode.isEmpty) {
await _kr_fetchUserInfo();
if (kr_referCode.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.getInviteCodeFailed);
return;
}
}
final qrContent = kr_getQRCodeContent();
if (qrContent.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.generateQRCodeFailed);
return;
}
// 只有在登录状态下才弹出二维码对话框
if (KRAppRunData.getInstance().kr_isLogin.value) {
Get.dialog(
Dialog(
backgroundColor: Theme.of(Get.context!).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppTranslations.kr_invite.shareQR,
style: KrAppTextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Theme.of(Get.context!).textTheme.titleMedium?.color,
),
),
SizedBox(height: 16.w),
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Theme.of(Get.context!).dividerColor.withOpacity(0.1),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: QrImageView(
data: qrContent,
version: QrVersions.auto,
size: 200.w,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
),
SizedBox(height: 20.w),
Container(
width: double.infinity,
height: 44.w,
child: TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(22.r),
),
),
child: Text(
AppTranslations.kr_invite.close,
style: TextStyle(
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
),
],
),
),
),
);
}
}
void kr_viewRewardDetails() {
// TODO: 实现查看奖励明细功能
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
void increment() => count.value++;
/// 处理链接分享
Future<void> kr_handleLinkShare() async {
if (!await kr_checkLoginStatus()) {
Get.find<KRMainController>().kr_setPage(0);
return;
}
if (kr_referCode.isEmpty) {
await _kr_fetchUserInfo();
if (kr_referCode.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.getInviteCodeFailed);
return;
}
}
final shareLink = kr_getShareLink();
if (shareLink.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.generateShareLinkFailed);
return;
}
await _kr_copyToClipboard(shareLink);
}
/// 处理复制邀请码
Future<void> kr_handleCopyInviteCode() async {
if (!await kr_checkLoginStatus()) {
Get.find<KRMainController>().kr_setPage(0);
return;
}
if (kr_referCode.isEmpty) {
await _kr_fetchUserInfo();
if (kr_referCode.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.getInviteCodeFailed);
return;
}
}
await _kr_copyToClipboard(kr_referCode.value);
}
/// 获取邀请统计信息
Future<void> _kr_fetchAffiliateCount() async {
try {
final either = await KRUserApi().kr_getAffiliateCount();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(affiliateCount) {
kr_progress.value = KRInviteProgress(
pending: kr_progress.value.pending,
processing: kr_progress.value.processing,
success: kr_progress.value.success,
expired: kr_progress.value.expired,
registers: affiliateCount.registers,
totalCommission: affiliateCount.totalCommission,
);
},
);
} catch (e) {
KRCommonUtil.kr_showToast(e.toString());
}
}
Future<void> kr_onRefresh() async {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
refreshController.finishRefresh();
return;
}
try {
await _kr_fetchUserInfo();
await _kr_fetchAffiliateCount();
} finally {
refreshController.finishRefresh();
}
}
}
+450
View File
@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.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:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:easy_refresh/easy_refresh.dart';
class _KRSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final double maxHeight;
final double minHeight;
_KRSliverPersistentHeaderDelegate({
required this.child,
required this.maxHeight,
required this.minHeight,
});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(_KRSliverPersistentHeaderDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
class KRInviteView extends GetView<KRInviteController> {
const KRInviteView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
body: EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
header: DeliveryHeader(
triggerOffset: 50.0,
springRebound: true,
),
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
expandedHeight: 150.w,
floating: false,
pinned: true,
stretch: true,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue,
Colors.blue.shade400,
Colors.blue.shade200,
Theme.of(context).primaryColor,
],
stops: const [0.0, 0.3, 0.7, 1.0],
),
),
),
Positioned(
bottom: -50.w,
child: KrLocalImage(
imageName: "invite_top_bg",
width: 344.w,
height: 233.w,
fit: BoxFit.contain,
imageType: ImageType.png,
),
),
Positioned(
top: MediaQuery.of(context).padding.top + 16.w,
left: 16.w,
child: Text(
AppTranslations.kr_invite.title,
style: KrAppTextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
SliverPersistentHeader(
delegate: _KRSliverPersistentHeaderDelegate(
maxHeight: 120.w,
minHeight: 120.w,
child: Container(
color: Theme.of(context).primaryColor,
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: _kr_buildProgressCard(context),
),
),
pinned: true,
),
SliverToBoxAdapter(
child: SingleChildScrollView(
child: Container(
color: Theme.of(context).primaryColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_kr_buildInviteSteps(context),
_kr_buildShareButtons(context),
_kr_buildInviteRules(context),
SizedBox(height: 20.w),
],
),
),
),
),
],
),
),
);
}
Widget _kr_buildProgressCard(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8.w,
offset: Offset(0, 2.w),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_invite.inviteStats,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Obx(() => Text(
controller.kr_progress.value.registers.toString(),
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.blue,
),
)),
),
],
),
SizedBox(height: 12.w),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_invite.registers,
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 4.w),
Obx(() => Text(
controller.kr_progress.value.registers.toString(),
style: KrAppTextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
)),
],
),
),
SizedBox(width: 16.w),
Expanded(
child: Visibility(
visible: AppConfig().kr_is_daytime,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_invite.totalCommission,
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 4.w),
Text(
controller.kr_progress.value.totalCommission.toString(),
style: KrAppTextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Colors.blue,
),
),
],
),
),
),
],
),
],
),
);
}
Widget _kr_buildInviteSteps(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(16.w, 32.w, 16.w, 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_invite.steps,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
SizedBox(height: 16.w),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_kr_buildStepCard(context, Icons.person_add, AppTranslations.kr_invite.inviteFriend),
_kr_buildStepCard(context, Icons.mail, AppTranslations.kr_invite.acceptInvite),
_kr_buildStepCard(context, Icons.card_giftcard, AppTranslations.kr_invite.getReward),
],
),
],
),
);
}
Widget _kr_buildStepCard(BuildContext context, IconData icon, String text) {
return Container(
width: 100.w,
padding: EdgeInsets.all(12.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10.r,
offset: Offset(0, 2.w),
),
],
),
child: Column(
children: [
Container(
width: 48.r,
height: 48.r,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.blue, size: 24.r),
),
SizedBox(height: 8.w),
Text(
text,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
Widget _kr_buildShareButtons(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.w),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.kr_handleLinkShare,
icon: Icon(Icons.link, size: 20.r, color: Colors.white),
label: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
AppTranslations.kr_invite.shareLink,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
),
),
),
SizedBox(width: 16.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.kr_handleQRShare,
icon: Icon(Icons.qr_code, size: 20.r, color: Colors.white),
label: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
AppTranslations.kr_invite.shareQR,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
),
),
),
],
),
SizedBox(height: 16.w),
Obx(() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${AppTranslations.kr_invite.myInviteCode}: ${controller.kr_referCode.value}',
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
IconButton(
icon: Icon(Icons.copy, size: 20.r, color: Colors.blue),
onPressed: () {
Clipboard.setData(ClipboardData(text: controller.kr_referCode.value));
KRCommonUtil.kr_showToast(
AppTranslations.kr_invite.inviteCodeCopied,
);
},
),
],
);
}),
],
),
);
}
Widget _kr_buildInviteRules(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_invite.rules,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.w),
Text(
AppTranslations.kr_invite.rule1,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 8.w),
Text(
AppTranslations.kr_invite.rule2,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_language_selector_controller.dart';
class KRLanguageSelectorBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRLanguageSelectorController>(
() => KRLanguageSelectorController(),
);
}
}
@@ -0,0 +1,42 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
class KRLanguageSelectorController extends GetxController {
// 使用 KRLanguage 枚举来加载语言
final RxList<KRLanguage> kr_languages = <KRLanguage>[].obs;
// 当前选中的语言代码
final RxString kr_selectedLanguage = ''.obs;
@override
void onInit() {
super.onInit();
kr_selectedLanguage.value = KRLanguageUtils.getCurrentLanguage().countryCode;
kr_loadLanguages();
}
// 加载语言数据
void kr_loadLanguages() {
// 将英语放在前面
final sortedLanguages = KRLanguage.values.toList()
..sort((a, b) => a == KRLanguage.en ? -1 : 1);
kr_languages.value = sortedLanguages;
}
// 选择语言
Future<void> kr_selectLanguage(KRLanguage language) async {
try {
// 先更新选中状态
kr_selectedLanguage.value = language.countryCode;
// 然后切换语言
await KRLanguageUtils.switchLanguage(language);
} catch (err) {
Get.snackbar(
'错误',
'切换语言失败: $err',
snackPosition: SnackPosition.BOTTOM,
);
}
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_language_selector_controller.dart';
class KRLanguageSelectorView extends GetView<KRLanguageSelectorController> {
const KRLanguageSelectorView({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,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_setting.switchLanguage,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => ListView.separated(
padding: EdgeInsets.all(16.r),
itemCount: controller.kr_languages.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final language = controller.kr_languages[index];
return _kr_buildLanguageCard(language, context);
},
),
),
),
],
),
),
);
}
// 构建语言卡片
Widget _kr_buildLanguageCard(KRLanguage language, BuildContext context) {
return InkWell(
onTap: () => controller.kr_selectLanguage(language),
child: Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
// 国旗图标
CircleAvatar(
radius: 16.r,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Text(
language.flagEmoji,
style: TextStyle(fontSize: 16),
),
),
SizedBox(width: 12.w),
// 语言名称
Text(
language.languageName,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
// 选中标记
if (controller.kr_selectedLanguage.value == language.countryCode)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.r,
),
],
),
),
);
}
}
+12
View File
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_login_controller.dart';
class MrLoginBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRLoginController>(
() => KRLoginController(),
);
}
}
@@ -0,0 +1,650 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
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 '../../../localization/kr_language_utils.dart';
/// 登录类型
enum KRLoginProgressStatus {
/// 检查是否注册
kr_check,
/// 验证码登陆
kr_loginByCode,
/// 密码登陆
kr_loginByPsd,
/// 注册发送验证码
kr_registerSendCode,
/// 这次设置密码
kr_registerSetPsd,
/// 忘记密码发送验证码
kr_forgetPsdSendCode,
/// 忘记密码设置密码
kr_forgetPsdSetPsd,
}
extension KRLoginTypeExt on KRLoginProgressStatus {
int get value {
switch (this) {
case KRLoginProgressStatus.kr_check:
return 0;
case KRLoginProgressStatus.kr_loginByCode:
return 2;
case KRLoginProgressStatus.kr_loginByPsd:
return 3;
case KRLoginProgressStatus.kr_registerSendCode:
return 4;
case KRLoginProgressStatus.kr_registerSetPsd:
return 5;
case KRLoginProgressStatus.kr_forgetPsdSendCode:
return 6;
case KRLoginProgressStatus.kr_forgetPsdSetPsd:
return 7;
}
}
}
class KRLoginController extends GetxController
with GetSingleTickerProviderStateMixin {
/// 是否注册
RxBool kr_isRegistered = false.obs;
/// 登陆类型
var kr_loginType = KRLoginType.kr_email.obs;
/// 登陆进度状态
var kr_loginStatus = KRLoginProgressStatus.kr_check.obs;
/// 验证码倒计时
var _countdown = 60; // 倒计时初始值
late Timer _timer;
var kr_countdownText = AppTranslations.kr_login.sendCode.obs;
/// 是否允许发送验证码
RxBool kr_canSendCode = true.obs;
/// 国家编码列表
late List<KRAreaCodeItem> kr_areaCodeList = KRAreaCode.kr_getCodeList();
var kr_cutSeleteCodeIndex = 0.obs;
/// 是否加密密码
var kr_obscureText = true.obs;
/// 匹配邮箱列表
RxList kr_emailList = [].obs;
RxBool kr_isDropdownVisible = false.obs;
/// 定位
final LayerLink kr_layerLink = LayerLink();
OverlayEntry? overlayEntry; // 悬浮框
bool isDropdownVisible = false; // 控制悬浮框显示状态
/// 动画
late AnimationController animationController;
late Animation<double> animation;
var height = 100.0.obs;
/// 账号编辑控制器
late TextEditingController accountController = TextEditingController();
/// 验证码编辑控制器
late TextEditingController codeController = TextEditingController();
/// 密码编辑控制器
late TextEditingController psdController = TextEditingController();
/// 密码编辑控制器
late TextEditingController agPsdController = TextEditingController();
var kr_accountHasText = false.obs;
var kr_codeHasText = false.obs;
var kr_psdHasText = false.obs;
var kr_agPsdHasText = false.obs;
// 添加邀请码相关控制
final TextEditingController inviteCodeController = TextEditingController();
final RxBool kr_inviteCodeHasText = false.obs;
// 添加 FocusNode
late FocusNode kr_accountFocusNode;
// 添加获取按钮文本的方法
String kr_getNextBtnText() {
switch (kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
return AppTranslations.kr_login.next;
case KRLoginProgressStatus.kr_loginByCode:
return AppTranslations.kr_login.codeLogin;
case KRLoginProgressStatus.kr_loginByPsd:
return AppTranslations.kr_login.passwordLogin;
case KRLoginProgressStatus.kr_registerSendCode:
return AppTranslations.kr_login.next;
case KRLoginProgressStatus.kr_registerSetPsd:
return AppTranslations.kr_login.registerNow;
case KRLoginProgressStatus.kr_forgetPsdSendCode:
return AppTranslations.kr_login.next;
case KRLoginProgressStatus.kr_forgetPsdSetPsd:
return AppTranslations.kr_login.setAndLogin;
}
}
@override
void onInit() {
super.onInit();
// 初始化计时器
_timer = Timer(Duration.zero, () {});
animationController = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
animation = Tween<double>(begin: 300.0, end: 300.0).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
)..addListener(() {
height.value = animation.value;
});
// 初始化 FocusNode
kr_accountFocusNode = FocusNode();
// 监听 kr_loginStatus 的变化
ever(kr_loginStatus, (status) {
switch (status) {
case KRLoginProgressStatus.kr_check:
kr_isDropdownVisible.value = true;
accountController.clear();
codeController.clear();
psdController.clear();
agPsdController.clear();
inviteCodeController.clear();
break;
case KRLoginProgressStatus.kr_loginByCode:
kr_isDropdownVisible.value = false;
break;
case KRLoginProgressStatus.kr_loginByPsd:
kr_isDropdownVisible.value = false;
break;
case KRLoginProgressStatus.kr_registerSendCode:
kr_isDropdownVisible.value = false;
break;
case KRLoginProgressStatus.kr_registerSetPsd:
kr_isDropdownVisible.value = false;
break;
case KRLoginProgressStatus.kr_forgetPsdSendCode:
kr_isDropdownVisible.value = false;
break;
case KRLoginProgressStatus.kr_forgetPsdSetPsd:
kr_isDropdownVisible.value = false;
break;
}
});
// 修改 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;
}
// 只在邮箱模式下更新邮箱列表
if (!isNumeric) {
kr_emailList.value = kr_generateAndSortEmailList(input);
}
kr_accountHasText.value = input.isNotEmpty;
});
});
/// 验证码
codeController.addListener(() {
kr_codeHasText.value = !codeController.text.isEmpty;
});
/// 密码
psdController.addListener(() {
kr_psdHasText.value = !psdController.text.isEmpty;
});
/// 密码
agPsdController.addListener(() {
kr_agPsdHasText.value = !agPsdController.text.isEmpty;
});
// 添加邀请码输入监听
inviteCodeController.addListener(() {
kr_inviteCodeHasText.value = inviteCodeController.text.isNotEmpty;
});
// 语言变化时更新所有翻译文本
ever(KRLanguageUtils.kr_language, (_) {
if (kr_canSendCode.value) {
kr_countdownText.value = "";
kr_countdownText.value = AppTranslations.kr_login.sendCode;
}
});
kr_initFocus();
}
// 判断是否是手机号
bool _isNumeric(String input) {
final numericRegex = RegExp(r'^\d+$'); // 匹配纯数字
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;
});
}
/// 发送验证码
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);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
/// 开始倒计时
_startCountdown();
});
}
/// 开始登录
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) {
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);
}, (r) async {
_saveLoginData(r);
});
}
/// 开始注册
void kr_register() async {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
if (agPsdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.reenterPassword);
return;
}
if (psdController.text != agPsdController.text) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.passwordMismatch);
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);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
_saveLoginData(r);
KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
});
}
void kr_checkCode() {
if (codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
return;
}
switch (kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
break;
case KRLoginProgressStatus.kr_loginByCode:
break;
case KRLoginProgressStatus.kr_loginByPsd:
break;
case KRLoginProgressStatus.kr_registerSendCode:
kr_checkVerificationCode(KRLoginProgressStatus.kr_registerSendCode);
break;
case KRLoginProgressStatus.kr_registerSetPsd:
break;
case KRLoginProgressStatus.kr_forgetPsdSendCode:
kr_checkVerificationCode(KRLoginProgressStatus.kr_forgetPsdSendCode);
break;
case KRLoginProgressStatus.kr_forgetPsdSetPsd:
break;
}
}
/// 验证验证码
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);
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) {
kr_loginStatus.value = KRLoginProgressStatus.kr_forgetPsdSetPsd;
}
});
}
/// 忘记密码--- 设置新密码
void kr_setNewPsdByForgetPsd() async {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
if (agPsdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.reenterPassword);
return;
}
if (psdController.text != agPsdController.text) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.passwordMismatch);
return;
}
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) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
codeController.clear();
psdController.clear();
kr_loginStatus.value = KRLoginProgressStatus.kr_forgetPsdSetPsd;
_saveLoginData(r);
});
}
/// 开始倒计时
void _startCountdown() {
kr_canSendCode.value = false;
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdown > 0) {
_countdown -= 1;
kr_countdownText.value = "${_countdown}s";
} else {
kr_canSendCode.value = true;
kr_countdownText.value = AppTranslations.kr_login.sendCode;
_countdown = 60;
timer.cancel();
_onCountdownFinished();
}
});
}
/// 设置登录数据
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);
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
}
/// 根据输入内容匹配邮箱
List<String> kr_generateAndSortEmailList(String input) {
// 常用邮箱域名
List<String> _commonEmailDomains = [
"@gmail.com",
"@yahoo.com",
"@outlook.com",
"@hotmail.com",
"@icloud.com",
"@aol.com",
"@zoho.com",
"@protonmail.com",
"@qq.com",
"@163.com",
"@126.com",
"@sina.com",
"@sohu.com",
"@foxmail.com",
"@aliyun.com",
"@189.cn",
"@china.com",
];
// 判断是否是邮箱格式
bool isEmail(String input) {
final emailRegex =
RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
return emailRegex.hasMatch(input);
}
// 输入过短或者是邮箱格式,直接返回空数组
if (input.length < 2 || isEmail(input)) {
return [];
}
// 处理输入,确保只保留一个 '@' 后的内容
String sanitizedInput =
input.contains("@") ? input.substring(0, input.indexOf("@")) : input;
// 根据匹配度排序
List<String> emailList = _commonEmailDomains.map((domain) {
return "$sanitizedInput$domain";
}).toList();
// 根据用户输入的 @ 后部分对域名进行匹配和排序
String? userDomain = input.contains("@") ? input.split("@").last : null;
if (userDomain != null && userDomain.isNotEmpty) {
emailList.sort((a, b) {
String domainA = a.split("@")[1];
String domainB = b.split("@")[1];
int matchScoreA = domainA.startsWith(userDomain) ? 1 : 0;
int matchScoreB = domainB.startsWith(userDomain) ? 1 : 0;
// 先比较匹配度,再按照字母顺序排序
if (matchScoreA == matchScoreB) {
return domainA.compareTo(domainB);
}
return matchScoreB.compareTo(matchScoreA);
});
}
return emailList;
}
/// 返回
void kr_back() {
kr_removeOverlay();
switch (kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
break;
case KRLoginProgressStatus.kr_loginByCode:
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
_resetTimer();
break;
case KRLoginProgressStatus.kr_loginByPsd:
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
break;
case KRLoginProgressStatus.kr_registerSendCode:
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
_resetTimer();
break;
case KRLoginProgressStatus.kr_registerSetPsd:
kr_loginStatus.value = KRLoginProgressStatus.kr_registerSendCode;
psdController.clear();
agPsdController.clear();
break;
case KRLoginProgressStatus.kr_forgetPsdSendCode:
kr_loginStatus.value = KRLoginProgressStatus.kr_loginByPsd;
_resetTimer();
codeController.clear();
psdController.clear();
agPsdController.clear();
break;
case KRLoginProgressStatus.kr_forgetPsdSetPsd:
kr_loginStatus.value = KRLoginProgressStatus.kr_forgetPsdSendCode;
psdController.clear();
agPsdController.clear();
break;
}
}
/// 重置计时器
void _resetTimer() {
if (_timer.isActive) {
_timer.cancel();
}
_countdown = 60;
kr_canSendCode.value = true;
kr_countdownText.value = AppTranslations.kr_login.sendCode;
}
void _onCountdownFinished() {}
void toggleHeight() {
// if (animationController.status == AnimationStatus.completed) {
// animationController.reverse();
// } else {
// animationController.forward();
// }
}
@override
void onClose() {
kr_removeOverlay();
if (_timer.isActive) {
_timer.cancel();
}
animationController.dispose();
inviteCodeController.dispose();
kr_accountFocusNode.dispose();
super.onClose();
}
// 添加点击输入框的方法
void kr_onInputTap() {
kr_accountFocusNode.requestFocus();
}
// 添加焦点管理
void kr_initFocus() {
kr_accountFocusNode.addListener(() {
if (kr_accountFocusNode.hasFocus) {
// 获得焦点时的处理
_updateInputState();
}
});
}
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);
}
}
// 添加移除悬浮框的方法
void kr_removeOverlay() {
overlayEntry?.remove();
overlayEntry = null;
}
}
@@ -0,0 +1,24 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/kr_area_code.dart'; // 假设这个文件中有 KRAreaCode 类
class KRSearchAreaController extends GetxController {
final areas = <KRAreaCodeItem>[].obs;
final searchQuery = ''.obs;
@override
void onInit() {
super.onInit();
areas.assignAll(KRAreaCode.kr_getCodeList());
}
List<KRAreaCodeItem> get filteredAreas {
if (searchQuery.value.isEmpty) {
return areas;
} else {
return areas
.where((area) => area.kr_dialCode.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/kr_area_code.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_search_area_controller.dart';
class KRSearchAreaView extends GetView<KRSearchAreaController> {
final Function(KRAreaCodeItem, int) onSelect;
const KRSearchAreaView({super.key, required this.onSelect});
static void show(Function(KRAreaCodeItem, int) onSelect) {
Get.dialog(
KRSearchAreaView(onSelect: onSelect),
barrierDismissible: true,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // 获取当前主题
Get.lazyPut<KRSearchAreaController>(
() => KRSearchAreaController(),
);
return GestureDetector(
onTap: () => Get.back(), // 点击背景关闭弹框
child: Scaffold(
backgroundColor: Colors.black.withOpacity(0.0),
body: Center(
child: GestureDetector(
onTap: () {}, // 阻止点击事件传递到背景
child: Container(
width: 300.w,
height: 450.w,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: theme.primaryColor,
borderRadius: BorderRadius.circular(15.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择其他地区',
style: KrAppTextStyle(
fontSize: 15.w,
fontWeight: FontWeight.w500,
color: theme.textTheme.titleMedium?.color),
),
SizedBox(height: 10.w),
TextField(
onChanged: (value) => controller.searchQuery.value = value,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: '搜索',
hintStyle: TextStyle(color: Colors.grey),
filled: true,
// fillColor: Colors.grey.shade200,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.w),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 10.w),
),
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 14.sp, fontFamily: 'AlibabaPuHuiTi-Regular',),
),
// SizedBox(height: 5.w),
Obx(() => Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: controller.filteredAreas.length,
itemBuilder: (context, index) {
final area = controller.filteredAreas[index];
return GestureDetector(
onTap: () {
onSelect(area, index); // 调用回调函数
Get.back();
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 10.w, horizontal: 0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.shade300,
width: 0.2,
),
),
),
child: Row(
children: [
Text(area.kr_icon,
style: TextStyle(fontSize: 20.w)),
SizedBox(width: 12.w),
Expanded(
child: Text(
area.kr_name,
style: KrAppTextStyle(
fontSize: 13.w,
fontWeight: FontWeight.w500),
),
),
Text(
"+" + area.kr_dialCode,
style: KrAppTextStyle(
fontSize: 13.w,
fontWeight: FontWeight.w500),
),
],
),
),
);
},
),
)),
],
),
),
),
),
),
);
}
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_invite/controllers/kr_invite_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_login/controllers/kr_login_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_statistics/controllers/kr_statistics_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_user_info/controllers/kr_user_info_controller.dart';
import '../controllers/kr_main_controller.dart';
class KRMainBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRMainController>(
() => KRMainController(),
);
Get.lazyPut(() => KRHomeController());
Get.lazyPut(() => KRLoginController());
Get.lazyPut(() => KRInviteController());
Get.lazyPut(() => KRUserInfoController());
Get.lazyPut(() => KRStatisticsController());
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_home/views/kr_home_view.dart';
import 'package:kaer_with_panels/app/modules/kr_invite/views/kr_invite_view.dart';
import 'package:kaer_with_panels/app/modules/kr_statistics/views/kr_statistics_view.dart';
import 'package:kaer_with_panels/app/modules/kr_user_info/views/kr_user_info_view.dart';
import 'package:kaer_with_panels/app/modules/kr_user_info/controllers/kr_user_info_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_keep_alive_wrapper.dart';
import '../../../widgets/kr_language_switch_dialog.dart';
enum MainRoutes {
INDEX(0, '首页'),
DYNAMICS(1, '看看'),
TOTALLETTER(2, '邮筒'),
MESSAGELIST(3, '消息'),
USER_CENTER(4, '我的');
final int i;
final String title;
const MainRoutes(this.i, this.title);
}
class KRMainController extends GetxController {
static KRMainController get to => Get.find();
DateTime? lastPopTime;
var kr_currentIndex = 0.obs;
final List<Widget> widgets = [
KRKeepAliveWrapper(KRHomeView()),
KRKeepAliveWrapper(KRInviteView()),
KRKeepAliveWrapper(KRStatisticsView()),
KRKeepAliveWrapper(KRUserInfoView()),
];
/// 分页控制器
PageController pageController = PageController(keepPage: true);
@override
void onInit() {
super.onInit();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
/// 到哪个页面,具体传值查看MainRoutes的枚举类
kr_setPage(int index) {
kr_currentIndex.value = index;
pageController.jumpToPage(index);
// 监控页面进入
if (index == MainRoutes.USER_CENTER.i) {
final userInfoController = Get.find<KRUserInfoController>();
userInfoController.kr_onPageEnter();
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_login/views/kr_login_view.dart';
import 'package:kaer_with_panels/app/modules/kr_main/views/kr_tabbar_view.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../controllers/kr_main_controller.dart';
class KRMainView extends GetView<KRMainController> {
const KRMainView({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // 获取当前主题
return Scaffold(
// 根据下标显示哪个页面
body: GetBuilder<KRMainController>(builder: (mc) {
return PageView(
children: mc.widgets,
controller: controller.pageController,
physics: const NeverScrollableScrollPhysics(),
);
}),
bottomNavigationBar: Obx(
() => KRCustomBottomNavBar(
backgroundColor: theme.scaffoldBackgroundColor,
currentIndex: controller.kr_currentIndex.value,
onTap: (i) => controller.kr_setPage(i),
items: [
KRCustomBottomNavBarItem(
imageName: "tab_home_n",
activeImageName: "tab_home_s",
),
KRCustomBottomNavBarItem(
imageName: "tab_invite_n",
activeImageName: "tab_invite_s",
),
KRCustomBottomNavBarItem(
imageName: "tab_statistics_n",
activeImageName: "tab_statistics_s",
),
KRCustomBottomNavBarItem(
imageName: "tab_my_n",
activeImageName: "tab_my_s",
),
],
),
),
);
}
}
+109
View File
@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
/// 用来描述底部导航栏的每一项
class KRCustomBottomNavBarItem {
/// 未选中时的图片名称
final String imageName;
/// 选中时的图片名称(可选,不传则用同一张 imageName)
final String? activeImageName;
/// 标签(可选)
final String? label;
KRCustomBottomNavBarItem({
required this.imageName,
this.activeImageName,
this.label,
});
}
class KRCustomBottomNavBar extends StatelessWidget {
/// 传入当前选中的索引
final int currentIndex;
/// 导航栏的各个条目信息
final List<KRCustomBottomNavBarItem> items;
/// 点击某项时的回调
final ValueChanged<int> onTap;
/// 背景色
final Color backgroundColor;
/// 选中时 图标/文字 颜色
final Color selectedColor;
/// 未选中时 图标/文字 颜色
final Color unselectedColor;
/// 导航栏高度(不含安全区额外高度)
final double height;
/// 是否在内部自动使用 SafeArea
final bool useSafeArea;
const KRCustomBottomNavBar({
Key? key,
required this.currentIndex,
required this.items,
required this.onTap,
this.backgroundColor = Colors.white,
this.selectedColor = Colors.blue,
this.unselectedColor = Colors.grey,
this.height = 56.0,
this.useSafeArea = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
assert(items.isNotEmpty, 'The items list cannot be empty.');
assert(currentIndex >= 0 && currentIndex < items.length,
'The currentIndex must be within the bounds of the items list.');
Widget child = Container(
color: backgroundColor,
height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(items.length, (index) {
final item = items[index];
final bool isSelected = (index == currentIndex);
final iconName = isSelected
? (item.activeImageName ?? item.imageName)
: item.imageName;
final textColor = isSelected ? selectedColor : unselectedColor;
return Expanded(
child: InkWell(
onTap: () => onTap(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
KrLocalImage(
imageName: iconName,
height: 24,
width: 24,
),
if (item.label != null) ...[
const SizedBox(height: 4),
Text(
item.label!,
style: TextStyle(color: textColor, fontSize: 12),
),
],
],
),
),
);
}),
),
);
// 使用 SafeArea 包裹,避免底部被手势栏遮挡
return useSafeArea ? SafeArea(child: child) : child;
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_message_controller.dart';
class KrMessageBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRMessageController>(
() => KRMessageController(),
);
}
}
@@ -0,0 +1,85 @@
import 'package:get/get.dart';
import 'package:easy_refresh/easy_refresh.dart';
import '../../../model/response/kr_message_list.dart';
import '../../../services/api_service/kr_api.user.dart';
import '../../../utils/kr_common_util.dart';
class KRMessageController extends GetxController {
final KRUserApi kr_userApi = KRUserApi();
// 通知列表数据
final RxList<KRMessage> kr_messages = <KRMessage>[].obs;
final RxBool kr_isLoading = false.obs;
final RxBool kr_hasMore = true.obs;
int kr_page = 1;
final int kr_size = 10;
final EasyRefreshController refreshController = EasyRefreshController();
@override
void onInit() {
super.onInit();
kr_getMessageList();
}
// 刷新列表
Future<void> kr_onRefresh() async {
kr_page = 1;
kr_hasMore.value = true;
kr_messages.clear();
await kr_getMessageList();
refreshController.finishRefresh();
}
// 加载更多
Future<void> kr_onLoadMore() async {
if (!kr_hasMore.value || kr_isLoading.value) {
refreshController.finishLoad(IndicatorResult.noMore);
return;
}
kr_page++;
await kr_getMessageList();
refreshController.finishLoad(kr_hasMore.value ? IndicatorResult.success : IndicatorResult.noMore);
}
Future<void> kr_getMessageList() async {
if (kr_isLoading.value) return;
kr_isLoading.value = true;
final either = await kr_userApi.kr_getMessageList(kr_page, kr_size);
either.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
if (kr_page > 1) kr_page--;
},
(list) {
if (list.announcements.isEmpty) {
kr_hasMore.value = false;
} else {
// 对消息进行排序,确保 pinned 为 true 的消息排在前面
final sortedMessages = List<KRMessage>.from(list.announcements)
..sort((a, b) {
// 首先按 pinned 状态排序
if (a.pinned != b.pinned) {
return a.pinned ? -1 : 1; // pinned 为 true 的排在前面
}
// 如果 pinned 状态相同,则按创建时间降序排序
return b.createdAt.compareTo(a.createdAt);
});
kr_messages.addAll(sortedMessages);
}
},
);
kr_isLoading.value = false;
}
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
}
+236
View File
@@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../model/response/kr_message_list.dart';
import '../controllers/kr_message_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
class KRMessageView extends GetView<KRMessageController> {
const KRMessageView({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,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_message.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
onLoad: controller.kr_onLoadMore,
header: DeliveryHeader(
triggerOffset: 50.0,
springRebound: true,
),
footer: DeliveryFooter(
triggerOffset: 50.0,
springRebound: true,
),
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.kr_messages.length,
itemBuilder: (context, index) {
final message = controller.kr_messages[index];
return _kr_buildMessageCard(message, context);
},
),
),
),
),
],
),
),
);
}
// 构建消息卡片
Widget _kr_buildMessageCard(KRMessage message, BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图标
Container(
width: 40.r,
height: 40.r,
decoration: BoxDecoration(
color:
// message.type == KRMessageType.system
Colors.blue,
// : Colors.orange,
shape: BoxShape.circle,
),
child: Icon(
Icons.notifications_outlined,
// : Icons.card_giftcard_outlined,
color: Colors.white,
size: 24.r,
),
),
SizedBox(width: 12.w),
// 内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
// message.type == KRMessageType.system
message.title,
// : AppTranslations.kr_message.promotion,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
Text(
message.kr_formattedCreatedAt,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
SizedBox(height: 4.h),
_kr_buildMessageContent(message.content, context),
],
),
),
],
),
);
}
// 构建消息内容
Widget _kr_buildMessageContent(String content, BuildContext context) {
// 判断内容类型
final bool kr_isHtml = content.contains('<') && content.contains('>');
final bool kr_isMarkdown = content.contains('**') ||
content.contains('*') ||
content.contains('#') ||
content.contains('- ') ||
content.contains('[');
final textStyle = KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
);
if (kr_isHtml) {
// 使用 flutter_html 处理 HTML 内容
return Html(
data: content,
style: {
'body': Style(
margin: Margins.all(0),
padding: HtmlPaddings.all(0),
fontSize: FontSize(12.sp),
color: Theme.of(context).textTheme.bodySmall?.color,
),
'p': Style(
margin: Margins.only(bottom: 8.h),
),
'b': Style(
fontWeight: FontWeight.bold,
),
'i': Style(
fontStyle: FontStyle.italic,
),
'a': Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
},
shrinkWrap: true,
);
} else if (kr_isMarkdown) {
// 使用 flutter_markdown 处理 Markdown 内容
return MarkdownBody(
data: content,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: 12.sp,
color: Theme.of(context).textTheme.bodySmall?.color,
),
strong: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodySmall?.color,
),
em: TextStyle(
fontSize: 12.sp,
fontStyle: FontStyle.italic,
color: Theme.of(context).textTheme.bodySmall?.color,
),
a: TextStyle(
fontSize: 12.sp,
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
);
} else {
// 普通文本
return Text(
content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: textStyle,
);
}
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_order_status_controller.dart';
/// 订单状态页面绑定
class KROrderStatusBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KROrderStatusController>(
() => KROrderStatusController(),
);
}
}
@@ -0,0 +1,181 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/api_service/kr_subscribe_api.dart';
import '../../../routes/app_pages.dart';
import '../../../model/response/kr_order_status.dart';
import '../../../utils/kr_event_bus.dart';
import '../../../utils/kr_common_util.dart';
import '../../../localization/app_translations.dart';
import '../../../utils/kr_log_util.dart';
/// 订单状态控制器
class KROrderStatusController extends GetxController {
/// API服务
final KRSubscribeApi kr_subscribeApi = KRSubscribeApi();
/// 支付是否成功
final RxBool kr_isPaymentSuccess = false.obs;
/// 是否正在加载
final RxBool kr_isLoading = true.obs;
/// 支付URL
final String kr_paymentUrl = Get.arguments['url'] as String;
/// 订单信息
final String kr_order = Get.arguments['order'];
/// 支付方式类型
final String kr_paymentType = Get.arguments['payment_type'] as String;
/// 定时器
Timer? kr_timer;
/// 订单状态常量
static const int kr_statusPending = 1; // 待支付
static const int kr_statusPaid = 2; // 已支付
static const int kr_statusClose = 3; // 已关闭
static const int kr_statusFailed = 4; // 支付失败
static const int kr_statusFinished = 5; // 已完成
/// 状态标题
final RxString kr_statusTitle = AppTranslations.kr_orderStatus.initialTitle.obs;
/// 状态描述
final RxString kr_statusDescription = AppTranslations.kr_orderStatus.initialDescription.obs;
/// 状态图标名称
final RxString kr_statusIcon = 'payment_success'.obs;
@override
void onInit() {
super.onInit();
kr_startCheckingPaymentStatus();
}
@override
void onReady() {
super.onReady();
// 只有在非余额支付且有支付URL时才处理支付跳转
if (kr_paymentUrl.isNotEmpty && kr_paymentType != 'balance') {
if (Platform.isAndroid || Platform.isIOS) {
// 移动端使用 WebView
Get.toNamed(
Routes.KR_WEBVIEW,
arguments: {
'url': kr_paymentUrl,
'order': kr_order,
},
);
} else {
// 桌面端使用外部浏览器
final Uri uri = Uri.parse(kr_paymentUrl);
launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
@override
void onClose() {
kr_timer?.cancel();
super.onClose();
}
/// 开始检查支付状态
void kr_startCheckingPaymentStatus() {
// 根据支付方式类型设置不同的查询间隔
final Duration interval = kr_paymentType == 'balance'
? const Duration(seconds: 2) // 余额支付每2秒查询一次
: const Duration(seconds: 5); // 其他支付方式每5秒查询一次
kr_timer = Timer.periodic(interval, (timer) {
kr_checkPaymentStatus();
});
}
/// 检查支付状态
Future<void> kr_checkPaymentStatus() async {
try {
final result = await kr_subscribeApi.kr_orderDetail(kr_order);
result.fold(
(error) {
KRLogUtil.kr_e('检查支付状态失败: $error', tag: 'OrderStatusController');
kr_isLoading.value = false;
kr_statusTitle.value = AppTranslations.kr_orderStatus.checkFailedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.checkFailedDescription;
kr_statusIcon.value = 'payment_success';
},
(kr_orderStatus) {
KRLogUtil.kr_i('检查支付状态: ${kr_orderStatus.toJson()}', tag: 'OrderStatusController');
switch (kr_orderStatus.kr_status) {
case kr_statusPending:
// 待支付状态,继续轮询
kr_statusTitle.value = AppTranslations.kr_orderStatus.pendingTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.pendingDescription;
kr_statusIcon.value = 'payment_success';
break;
case kr_statusPaid:
// 已支付状态,继续轮询直到完成
kr_statusTitle.value = AppTranslations.kr_orderStatus.paidTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.paidDescription;
kr_statusIcon.value = 'payment_success';
break;
case kr_statusFinished:
// 订单完成
kr_isPaymentSuccess.value = true;
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.successTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.successDescription;
kr_statusIcon.value = 'payment_success';
KREventBus().kr_sendMessage(KRMessageType.kr_payment);
break;
case kr_statusClose:
// 订单已关闭
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.closedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.closedDescription;
kr_statusIcon.value = 'payment_success';
break;
case kr_statusFailed:
// 支付失败
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.failedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.failedDescription;
kr_statusIcon.value = 'payment_success';
break;
default:
// 未知状态
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.unknownTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.unknownDescription;
kr_statusIcon.value = 'payment_success';
break;
}
},
);
} catch (error) {
KRLogUtil.kr_e('检查支付状态失败: $error', tag: 'OrderStatusController');
kr_isLoading.value = false;
kr_statusTitle.value = AppTranslations.kr_orderStatus.checkFailedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.checkFailedDescription;
kr_statusIcon.value = 'payment_success';
}
}
/// 检查支付状态
Future<void> kr_checkPaymentStatusWithRetry() async {
try {
// ... 其他代码 ...
} catch (err) {
KRLogUtil.kr_e('检查支付状态失败: $err', tag: 'OrderStatusController');
}
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_order_status_controller.dart';
/// 订单状态视图
class KROrderStatusView extends GetView<KROrderStatusController> {
const KROrderStatusView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: Text(
AppTranslations.kr_orderStatus.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
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: Obx(
() => SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 60.h),
// 状态图标
_buildStatusIcon(),
SizedBox(height: 32.h),
// 状态文本
_buildStatusText(),
SizedBox(height: 16.h),
// 描述文本
_buildDescriptionText(),
const Spacer(),
],
),
),
),
),
),
);
}
/// 构建状态图标
Widget _buildStatusIcon() {
return KrLocalImage(
imageName: controller.kr_statusIcon.value,
width: 160.w,
height: 160.w,
);
}
/// 构建状态文本
Widget _buildStatusText() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Text(
controller.kr_statusTitle.value,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
);
}
/// 构建描述文本
Widget _buildDescriptionText() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Text(
controller.kr_statusDescription.value,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
).copyWith(height: 1.5),
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_purchase_membership_controller.dart';
class KRPurchaseMembershipBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRPurchaseMembershipController>(
() => KRPurchaseMembershipController(),
);
}
}
@@ -0,0 +1,506 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_subscribe_api.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.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_log_util.dart';
import '../../../common/app_run_data.dart';
import '../../../model/response/kr_already_subscribe.dart';
import '../../../model/response/kr_payment_methods.dart';
import '../../../routes/app_pages.dart';
import '../../../services/api_service/kr_api.user.dart';
import '../../../utils/kr_event_bus.dart';
/// 会员购买控制器
/// 负责处理会员套餐选择、支付方式选择和订阅流程
class KRPurchaseMembershipController extends GetxController {
// 注入的服务
final KRSubscribeApi _kr_subscribeApi = KRSubscribeApi();
// 事件监听器
Worker? _kr_eventWorker;
// UI 状态
final RxBool kr_isLoading = false.obs;
final RxString kr_errorMessage = ''.obs;
final RxString kr_userEmail = ''.obs;
final RxBool kr_showPlanSelector = false.obs; // 是否显示套餐选择器
// 数据状态
final RxList<KRPackageListItem> kr_plans = <KRPackageListItem>[].obs;
final RxList<KRPaymentMethod> kr_paymentMethods = <KRPaymentMethod>[].obs;
final RxInt kr_selectedPlanIndex = 0.obs;
final RxInt kr_selectedPaymentMethodIndex = (-1).obs;
final RxInt kr_selectedDiscountIndex = (-1).obs;
// 已订阅套餐列表
var _kr_alreadySubscribe = <KRAlreadySubscribe>[];
/// 描述是否展开
final kr_isDescriptionExpanded = false.obs;
/// 当前余额
var _kr_balance = 0;
@override
void onInit() {
super.onInit();
kr_initializeData();
}
@override
void onClose() {
_kr_eventWorker?.dispose();
super.onClose();
}
/// 初始化数据
Future<void> kr_initializeData() async {
kr_userEmail.value = KRAppRunData.getInstance().kr_account.toString();
await kr_getPackageList();
// 监听所有支付相关消息
_kr_eventWorker = KREventBus().kr_listenMessages(
[KRMessageType.kr_payment, KRMessageType.kr_subscribe_update],
_kr_handleMessage,
);
}
/// 处理消息
Future<void> _kr_handleMessage(KRMessageData message) async {
switch (message.kr_type) {
case KRMessageType.kr_payment:
await _iniUserInfo();
// 只更新支付方式显示,因为支付方式标题中包含余额信息
if (kr_paymentMethods.isNotEmpty) {
final balanceMethodIndex = kr_paymentMethods
.indexWhere((method) => method.platform == 'balance');
if (balanceMethodIndex != -1) {
// 触发支付方式列表更新
kr_paymentMethods.refresh();
}
}
break;
case KRMessageType.kr_subscribe_update:
break;
default:
break;
}
}
/// 获取套餐列表和支付方式
Future<void> kr_getPackageList() async {
kr_isLoading.value = true;
kr_selectedPlanIndex.value = 0; // 重置套餐选择
kr_selectedDiscountIndex.value = -1; // 重置折扣选择
kr_selectedPaymentMethodIndex.value = -1; // 重置支付方式选择
await _iniUserInfo();
await kr_getAlreadySubscribe();
await kr_fetchPackages();
await kr_fetchPaymentMethods();
// 根据套餐数量决定是否显示套餐选择器
kr_showPlanSelector.value = kr_plans.length > 1;
kr_isLoading.value = false;
}
/// 初始化用户信息
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;
},
);
}
/// 获取用户已订阅套餐
Future<void> kr_getAlreadySubscribe() async {
final either = await _kr_subscribeApi.kr_getAlreadySubscribe();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(alreadySubscribe) {
_kr_alreadySubscribe = alreadySubscribe;
KRLogUtil.kr_i(
'已订阅套餐: ${_kr_alreadySubscribe.map((e) => e.subscribeId).toList()}',
tag: 'PurchaseMembershipController');
},
);
}
/// 获取套餐列表
Future<void> kr_fetchPackages() async {
final either = await _kr_subscribeApi.kr_getPackageListList();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(packageList) {
kr_plans.value = packageList.kr_list;
// 默认选择第一个套餐
if (kr_plans.isNotEmpty) {
kr_selectedPlanIndex.value = 0;
kr_initializeSelection(kr_plans.first);
}
},
);
}
/// 获取支付方式列表
Future<void> kr_fetchPaymentMethods() async {
final either = await _kr_subscribeApi.kr_getPaymentMethods();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(paymentMethods) {
kr_paymentMethods.value = paymentMethods;
// 检查当前选择的套餐价格是否小于等于余额
if (kr_plans.isNotEmpty) {
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final selectedPrice = kr_getPlanPrice(selectedPlan,
discountIndex: kr_selectedDiscountIndex.value);
// 查找余额支付方式的索引
final balanceMethodIndex = paymentMethods
.indexWhere((method) => method.platform == 'balance');
// 如果找到余额支付方式且余额足够,默认选择余额支付
if (balanceMethodIndex != -1 && selectedPrice <= _kr_balance / 100) {
kr_selectPaymentMethod(balanceMethodIndex);
} else {
// 查找第一个非余额支付方式
final nonBalanceMethodIndex = paymentMethods
.indexWhere((method) => method.platform != 'balance');
if (nonBalanceMethodIndex != -1) {
kr_selectPaymentMethod(nonBalanceMethodIndex);
}
}
}
},
);
}
/// 获取支付方式显示标题
String kr_getPaymentMethodTitle(KRPaymentMethod method) {
if (method.platform == 'balance') {
return '${method.name}${(_kr_balance / 100).toStringAsFixed(2)})';
}
return method.name;
}
/// 选择套餐
void kr_selectPlan(int planIndex, {int? discountIndex}) {
if (planIndex >= 0 && planIndex < kr_plans.length) {
kr_selectedPlanIndex.value = planIndex;
// 确保折扣索引有效
if (discountIndex != null) {
final plan = kr_plans[planIndex];
if (discountIndex >= 0 && discountIndex < plan.kr_discount.length) {
kr_selectedDiscountIndex.value = discountIndex;
} else {
// 如果传入的折扣索引无效,但有折扣选项,则默认选择第一个
if (plan.kr_discount.isNotEmpty) {
kr_selectedDiscountIndex.value = 0;
} else {
kr_selectedDiscountIndex.value = -1;
}
}
} else {
// 如果没有传入折扣索引,但有折扣选项,则默认选择第一个
final plan = kr_plans[planIndex];
if (plan.kr_discount.isNotEmpty) {
kr_selectedDiscountIndex.value = 0;
} else {
kr_selectedDiscountIndex.value = -1;
}
}
// 重置支付方式选择
kr_selectedPaymentMethodIndex.value = -1;
// 重新判断应该选择的支付方式
_kr_updatePaymentMethodSelection();
// 更新UI状态
update();
}
}
/// 更新支付方式选择
void _kr_updatePaymentMethodSelection() {
if (kr_plans.isEmpty || kr_paymentMethods.isEmpty) return;
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final selectedPrice = kr_getPlanPrice(selectedPlan,
discountIndex: kr_selectedDiscountIndex.value);
// 查找余额支付方式的索引
final balanceMethodIndex =
kr_paymentMethods.indexWhere((method) => method.platform == 'balance');
// 如果找到余额支付方式且余额足够,选择余额支付
if (balanceMethodIndex != -1 && selectedPrice <= _kr_balance / 100) {
kr_selectPaymentMethod(balanceMethodIndex);
} else {
// 查找第一个非余额支付方式
final nonBalanceMethodIndex = kr_paymentMethods
.indexWhere((method) => method.platform != 'balance');
if (nonBalanceMethodIndex != -1) {
kr_selectPaymentMethod(nonBalanceMethodIndex);
}
}
}
/// 选择支付方式
void kr_selectPaymentMethod(int index) {
if (index >= 0 && index < kr_paymentMethods.length) {
kr_selectedPaymentMethodIndex.value = index;
}
}
/// 获取当前选中的数量
int kr_getSelectedQuantity() {
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
if (kr_selectedDiscountIndex.value >= 0 &&
kr_selectedDiscountIndex.value < selectedPlan.kr_discount.length) {
return selectedPlan
.kr_discount[kr_selectedDiscountIndex.value].kr_quantity;
}
return 1; // 默认数量为1
}
/// 开始订阅流程
Future<void> kr_startSubscription() async {
if (!kr_validateSubscriptionData()) return;
kr_errorMessage.value = '';
try {
await kr_processPurchaseAndCheckout();
} catch (e) {
kr_errorMessage.value = '订阅失败: ${e.toString()}';
}
}
/// 验证订阅数据
bool kr_validateSubscriptionData() {
if (kr_plans.isEmpty) {
KRCommonUtil.kr_showToast('没有可用的套餐');
return false;
}
if (kr_selectedPaymentMethodIndex.value < 0 ||
kr_selectedPaymentMethodIndex.value >= kr_paymentMethods.length) {
KRCommonUtil.kr_showToast('请选择支付方式');
return false;
}
return true;
}
/// 处理购买和结账流程
Future<void> kr_processPurchaseAndCheckout() async {
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final selectedPaymentMethod =
kr_paymentMethods[kr_selectedPaymentMethodIndex.value];
// 获取选中的数量
final quantity = kr_getSelectedQuantity();
// 判断是续订还是新购
final isRenewal = _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
final subscribeId = isRenewal
? _kr_alreadySubscribe
.firstWhere(
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id)
.userSubscribeId
: 0;
// 根据判断结果调用不同的接口
final purchaseEither = isRenewal
? await _kr_subscribeApi.kr_renewal(
subscribeId,
quantity,
selectedPaymentMethod.id,
'',
)
: await _kr_subscribeApi.kr_purchase(
selectedPlan.kr_id,
quantity,
selectedPaymentMethod.id,
'',
);
purchaseEither.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(order) async {
// 所有支付方式都需要调用 checkout 接口
final checkoutEither = await _kr_subscribeApi.kr_checkout(order);
checkoutEither.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(uri) => Get.toNamed(
Routes.KR_ORDER_STATUS,
arguments: {
'url': uri,
'order': order,
'payment_type': selectedPaymentMethod.platform,
},
),
);
},
);
}
/// 获取套餐价格
double kr_getPlanPrice(KRPackageListItem plan, {int? discountIndex}) {
if (discountIndex != null &&
discountIndex >= 0 &&
discountIndex < plan.kr_discount.length) {
// 计算折扣价格
final discount = plan.kr_discount[discountIndex];
return (plan.kr_unitPrice / 100) *
discount.kr_quantity *
(discount.kr_discount / 100);
}
return plan.kr_unitPrice / 100;
}
/// 获取时间字符串
String kr_getTimeStr(KRPackageListItem plan, {int? discountIndex}) {
final quantity = discountIndex != null &&
discountIndex >= 0 &&
discountIndex < plan.kr_discount.length
? plan.kr_discount[discountIndex].kr_quantity
: 1;
if (plan.kr_unitTime == 'Month') {
return AppTranslations.kr_purchaseMembership.month(quantity);
} else if (plan.kr_unitTime == 'Year') {
return AppTranslations.kr_purchaseMembership.year(quantity);
} else if (plan.kr_unitTime == 'Day') {
return AppTranslations.kr_purchaseMembership.day(quantity);
}
return '';
}
/// 获取折扣文本
String kr_getDiscountText(KRPackageListItem plan, int discountIndex) {
if (discountIndex >= 0 && discountIndex < plan.kr_discount.length) {
final discount = plan.kr_discount[discountIndex];
// 折扣值为 100 表示原价,不需要显示折扣
if (discount.kr_discount == 100) {
return '';
}
// 计算折扣百分比(例如:95% 显示为 -5%)
final discountPercent = 100 - discount.kr_discount;
return '-${discountPercent}%';
}
return '';
}
/// 获取套餐总选项数
int kr_getTotalOptionsCount(KRPackageListItem plan) {
// 确保折扣列表不为空
if (plan.kr_discount.isEmpty) {
return 1; // 如果没有折扣选项,至少返回1个选项
}
return plan.kr_discount.length;
}
/// 初始化选择
void kr_initializeSelection(KRPackageListItem plan) {
if (plan.kr_discount.isNotEmpty) {
// 默认选择第一个选项
kr_selectedDiscountIndex.value = 0;
} else {
// 如果没有选项,设置为 -1
kr_selectedDiscountIndex.value = -1;
}
}
/// 获取当前选中套餐的描述
String kr_getSelectedPlanDescription() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return '';
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features
.map((feature) => feature.kr_label)
.join('');
}
/// 获取当前选中套餐的特性标题列表
List<String> kr_getSelectedPlanFeatureLabels() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return [];
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features
.map((feature) => feature.kr_label)
.toList();
}
/// 获取当前选中套餐的详细信息
List<KRFeature> kr_getSelectedPlanFeatures() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return [];
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features;
}
/// 判断当前选中的套餐是否是续订
bool kr_isRenewal() {
if (kr_plans.isEmpty || _kr_alreadySubscribe.isEmpty) return false;
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
return _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
}
/// 获取当前选中套餐的订阅按钮文字
String kr_getSubscribeButtonText() {
if (kr_plans.isEmpty) return '';
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final isRenewal = _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
return isRenewal
? AppTranslations.kr_purchaseMembership.renewNow
: AppTranslations.kr_purchaseMembership.startSubscription;
}
/// 切换描述展开状态
void kr_toggleDescriptionExpanded() {
kr_isDescriptionExpanded.value = !kr_isDescriptionExpanded.value;
}
/// 获取流量限制显示文本
String kr_getTrafficLimitText(KRPackageListItem plan) {
KRLogUtil.kr_i('原始流量值: ${plan.kr_traffic}', tag: 'TrafficLimit');
if (plan.kr_traffic == 0) {
return AppTranslations.kr_purchaseMembership.unlimitedTraffic;
}
// 将字节转换为GB
final trafficInGB = plan.kr_traffic / (1024 * 1024 * 1024);
KRLogUtil.kr_i('转换为GB后的值: $trafficInGB', tag: 'TrafficLimit');
if (trafficInGB < 1) {
return '${(trafficInGB * 1024).toStringAsFixed(0)}MB';
} else if (trafficInGB < 1024) {
return '${trafficInGB.toStringAsFixed(0)}GB';
} else {
return '${(trafficInGB / 1024).toStringAsFixed(1)}TB';
}
}
/// 获取设备限制显示文本
String kr_getDeviceLimitText(KRPackageListItem plan) {
if (plan.kr_deviceLimit == 0) {
return AppTranslations.kr_purchaseMembership.unlimitedDevices;
}
return AppTranslations.kr_purchaseMembership
.devices(plan.kr_deviceLimit.toString());
}
}
@@ -0,0 +1,796 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_purchase_membership_controller.dart';
import '../../../widgets/kr_simple_loading.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import '../../../widgets/kr_network_image.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
/// 购买会员页面视图
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController> {
const KRPurchaseMembershipView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Obx(() {
return 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,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_purchaseMembership.purchasePackage,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
_kr_buildAccountSection(context),
if (controller.kr_isLoading.value)
Container(
height: MediaQuery.of(context).size.height * 0.5,
child: Center(
child: KRSimpleLoading(
color: Colors.blue,
size: 50.0,
),
),
)
else if (controller.kr_plans.isEmpty)
Container(
height: MediaQuery.of(context).size.height * 0.5,
child: Center(
child: Text(
AppTranslations.kr_purchaseMembership.noData,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
)
else
Container(
margin: EdgeInsets.symmetric(horizontal: 16.r),
child: Column(
children: [
// 套餐选择部分
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_purchaseMembership.selectPackage,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
if (controller.kr_plans.length > 1)
Container(
height: 32.h,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
itemCount: controller.kr_plans.length,
itemBuilder: (context, index) {
final plan = controller.kr_plans[index];
final isSelected = index == controller.kr_selectedPlanIndex.value;
return GestureDetector(
onTap: () => controller.kr_selectPlan(index),
child: Container(
margin: EdgeInsets.only(right: 8.w),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.withOpacity(0.3),
width: 1,
),
),
child: Center(
child: Text(
plan.kr_name,
style: KrAppTextStyle(
fontSize: 13,
color: isSelected ? Colors.white : Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
),
);
},
),
),
SizedBox(height: 16.h),
GridView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (Platform.isWindows || Platform.isMacOS || Platform.isLinux) ? 2.0 : 0.85,
crossAxisSpacing: 8.w,
mainAxisSpacing: 8.h,
),
itemCount: controller.kr_getTotalOptionsCount(controller.kr_plans[controller.kr_selectedPlanIndex.value]),
itemBuilder: (context, index) {
final plan = controller.kr_plans[controller.kr_selectedPlanIndex.value];
final discountIndex = plan.kr_discount.isEmpty ? null : index;
return _kr_buildPlanOptionCard(
plan,
controller.kr_selectedPlanIndex.value,
discountIndex,
context,
);
},
),
],
),
),
SizedBox(height: 16.h),
// 套餐描述部分
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_purchaseMembership.packageDescription,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
Obx(() {
final featureLabels = controller.kr_getSelectedPlanFeatureLabels();
final features = controller.kr_getSelectedPlanFeatures();
final isExpanded = controller.kr_isDescriptionExpanded.value;
final selectedPlan = controller.kr_plans[controller.kr_selectedPlanIndex.value];
// 添加流量和设备限制信息
final trafficAndDeviceInfo = Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${AppTranslations.kr_purchaseMembership.trafficLimit}${controller.kr_getTrafficLimitText(selectedPlan)}',
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 4.h),
Text(
'${AppTranslations.kr_purchaseMembership.deviceLimit}${controller.kr_getDeviceLimitText(selectedPlan)}',
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
],
),
),
);
// if (featureLabels.isEmpty) {
// return Column(
// children: [
// trafficAndDeviceInfo,
// Center(
// child: Padding(
// padding: EdgeInsets.symmetric(vertical: 16.h),
// child: Text(
// AppTranslations.kr_purchaseMembership.noData,
// style: KrAppTextStyle(
// fontSize: 14,
// color: Theme.of(context).textTheme.bodySmall?.color,
// ),
// ),
// ),
// ),
// ],
// );
// }
final displayCount = isExpanded ? featureLabels.length : (featureLabels.length > 3 ? 3 : featureLabels.length);
return Column(
children: [
trafficAndDeviceInfo,
...List.generate(displayCount, (index) {
final feature = features[index];
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onTap: () {
Get.dialog(
Dialog(
backgroundColor: Theme.of(Get.context!).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.all(16.r),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.kr_label,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 8.h),
if (feature.kr_details.isNotEmpty)
...feature.kr_details.map((detail) => Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (detail.kr_label.isNotEmpty) ...[
Text(
detail.kr_label,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 4.h),
],
Text(
detail.kr_description,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
),
),
],
),
)).toList(),
SizedBox(height: 16.h),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => Get.back(),
child: Text(
AppTranslations.kr_dialog.kr_ok,
style: KrAppTextStyle(
fontSize: 14,
color: Colors.blue,
),
),
),
),
],
),
),
),
);
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Theme.of(Get.context!).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: Row(
children: [
Expanded(
child: Text(
featureLabels[index],
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
),
Container(
padding: EdgeInsets.all(4.r),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.info_outline,
size: 16.r,
color: Colors.blue,
),
),
],
),
),
),
);
}),
if (featureLabels.length > 3)
GestureDetector(
onTap: () => controller.kr_toggleDescriptionExpanded(),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isExpanded
? AppTranslations.kr_purchaseMembership.collapse
: AppTranslations.kr_purchaseMembership.expand,
style: KrAppTextStyle(
fontSize: 13,
color: Colors.blue,
),
),
SizedBox(width: 4.w),
Icon(
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
size: 16.r,
color: Colors.blue,
),
],
),
),
),
],
);
}),
],
),
),
SizedBox(height: 16.h),
// 支付方式选择部分
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_purchaseMembership.paymentMethod,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
Obx(() => ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: controller.kr_paymentMethods.length,
separatorBuilder: (context, index) => Divider(
height: 1.w,
indent: 44.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
itemBuilder: (context, index) {
final paymentMethod = controller.kr_paymentMethods[index];
return InkWell(
onTap: () => controller.kr_selectPaymentMethod(index),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.r, horizontal: 16.r),
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: paymentMethod.icon.isNotEmpty
? KRNetworkImage(
kr_imageUrl: paymentMethod.icon,
kr_width: 20.w,
kr_height: 20.w,
kr_placeholder: SizedBox(
width: 20.w,
height: 20.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
kr_errorWidget: Icon(
Icons.payment_rounded,
size: 20.w,
color: Colors.blue,
),
)
: Icon(
Icons.payment_rounded,
size: 20.w,
color: Colors.blue,
),
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_getPaymentMethodTitle(paymentMethod),
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
Obx(() {
final isSelected = index == controller.kr_selectedPaymentMethodIndex.value;
return Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.transparent,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.withOpacity(0.3),
width: 1.5,
),
),
child: isSelected
? Icon(
Icons.check,
color: Colors.white,
size: 16.r,
)
: null,
);
}),
],
),
),
);
},
)),
],
),
),
SizedBox(height: 80.h), // 为底部按钮留出空间
],
),
),
],
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _kr_buildBottomSection(context),
),
],
),
),
],
),
);
}),
);
}
// 账号部分
Widget _kr_buildAccountSection(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.r),
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Text(
AppTranslations.kr_purchaseMembership.myAccount,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
Obx(() => Text(
controller.kr_userEmail.value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
)),
],
),
);
}
// 套餐选项卡片
Widget _kr_buildPlanOptionCard(
KRPackageListItem plan,
int planIndex,
int? discountIndex,
BuildContext context) {
return Obx(() {
bool isSelected = planIndex == controller.kr_selectedPlanIndex.value &&
discountIndex == controller.kr_selectedDiscountIndex.value;
return GestureDetector(
onTap: () => controller.kr_selectPlan(planIndex, discountIndex: discountIndex),
child: Container(
padding: EdgeInsets.symmetric(vertical: 10.h),
decoration: BoxDecoration(
color: isSelected
? Colors.blue.withOpacity(0.08)
: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isSelected ? Colors.blue.withOpacity(0.3) : Colors.grey.withOpacity(0.15),
width: 1,
),
boxShadow: isSelected ? [
BoxShadow(
color: Colors.blue.withOpacity(0.08),
blurRadius: 12,
offset: Offset(0, 4),
spreadRadius: 0,
),
] : [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: Offset(0, 2),
spreadRadius: 0,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.kr_getTimeStr(plan, discountIndex: discountIndex),
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isSelected
? Colors.blue
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 6.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'¥',
style: KrAppTextStyle(
fontSize: 12,
color: isSelected
? Colors.blue.withOpacity(0.8)
: Theme.of(context).textTheme.bodySmall?.color,
),
),
Text(
controller.kr_getPlanPrice(plan, discountIndex: discountIndex)
.toStringAsFixed(2),
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.blue
: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
SizedBox(height: 6.h),
if (discountIndex != null && plan.kr_discount.isNotEmpty) ...[
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h
),
decoration: BoxDecoration(
color: plan.kr_discount[discountIndex].kr_discount < 100
? Colors.red.withOpacity(0.08)
: Colors.transparent,
borderRadius: BorderRadius.circular(4.r),
border: plan.kr_discount[discountIndex].kr_discount < 100
? Border.all(
color: Colors.red.withOpacity(0.2),
width: 1,
)
: null,
boxShadow: plan.kr_discount[discountIndex].kr_discount < 100
? [
BoxShadow(
color: Colors.red.withOpacity(0.05),
blurRadius: 4,
offset: Offset(0, 2),
spreadRadius: 0,
),
]
: null,
),
child: Text(
controller.kr_getDiscountText(plan, discountIndex),
style: KrAppTextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: plan.kr_discount[discountIndex].kr_discount < 100
? Colors.red.withOpacity(0.9)
: Colors.transparent,
),
),
),
],
],
),
),
);
});
}
// 底部部分
Widget _kr_buildBottomSection(BuildContext context) {
// 如果正在加载或没有数据,不显示底部按钮
if (controller.kr_isLoading.value || controller.kr_plans.isEmpty || controller.kr_paymentMethods.isEmpty) {
return SizedBox.shrink();
}
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: Offset(0, -2),
blurRadius: 8,
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
KRDialog.show(
title: AppTranslations.kr_purchaseMembership.confirmPurchase,
message: AppTranslations.kr_purchaseMembership.confirmPurchaseDesc,
cancelText: AppTranslations.kr_dialog.kr_cancel,
confirmText: AppTranslations.kr_dialog.kr_confirm,
onConfirm: () => controller.kr_startSubscription(),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
controller.kr_getSubscribeButtonText(),
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
);
}
}
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
/// 套餐详情弹框
class KRPlanDetailsDialog extends StatelessWidget {
final List<KRFeature> kr_features;
const KRPlanDetailsDialog({
Key? key,
required this.kr_features,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_purchaseMembership.planDetails,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: kr_features.length,
itemBuilder: (context, index) {
final feature = kr_features[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.kr_label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
...feature.kr_details.map((detail) => Padding(
padding: const EdgeInsets.only(left: 16, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.check_circle_outline,
size: 16,
color: Colors.green,
),
const SizedBox(width: 8),
Expanded(
child: Text(
detail.kr_description,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
)),
if (index < kr_features.length - 1)
const Divider(height: 24),
],
);
},
),
),
],
),
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_setting_controller.dart';
class KRSettingBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRSettingController>(
() => KRSettingController(),
);
}
}
@@ -0,0 +1,170 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import '../../../localization/app_translations.dart';
import '../../../themes/kr_theme_service.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRSettingController extends GetxController {
// 创建 AppTranslationsSetting 的实例
final AppTranslationsSetting kr_appTranslationsSetting =
AppTranslationsSetting();
// 当前选择的国家
final RxString kr_currentCountry = ''.obs;
// 自动连接开关
final RxBool kr_autoConnect = true.obs;
// 通知开关
final RxBool kr_notification = true.obs;
// 帮助改进开关
final RxBool kr_helpImprove = true.obs;
// 版本号
final RxString kr_version = ''.obs;
// IOS评分
final String kr_iosRating = '';
// 当前语言
final RxString kr_language = ''.obs;
// 当前主题选项
final RxString kr_themeOption = ''.obs;
final RxString kr_vpnMode = ''.obs;
final RxString kr_vpnModeRemark = ''.obs;
// 修改 VPN 模式切换方法
void kr_changeVPNMode(String mode) {
KRLogUtil.kr_i('设置的VPN模式文本: ${kr_vpnMode.value}', tag: 'SettingController');
}
// 切换语言
void kr_changeLanguage() {
Get.toNamed(Routes.KR_LANGUAGE_SELECTOR);
}
// 删除账号
void kr_deleteAccount() {
// 检查是否已登录
if (!KRAppRunData.getInstance().kr_isLogin.value) {
// 如果未登录,跳转到登录页面
// Get.toNamed(Routes.MR_LOGIN);
return;
}
// 已登录,跳转到删除账号页面
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
}
@override
void onInit() {
super.onInit();
_loadThemeOption();
kr_language.value = KRLanguageUtils.getCurrentLanguage().languageName;
// 语言变化时更新所有翻译文本
ever(KRLanguageUtils.kr_language, (_) {
kr_language.value = KRLanguageUtils.kr_language.value;
_loadThemeOption();
kr_currentCountry.value = "";
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
kr_vpnMode.value = '';
kr_vpnMode.value =
kr_getConnectionTypeString(KRSingBoxImp().kr_connectionType.value);
kr_vpnModeRemark.value = '';
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(KRSingBoxImp().kr_connectionType.value);
});
ever(KRCountryUtil.kr_currentCountry, (_) {
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
});
kr_currentCountry.value = KRCountryUtil.kr_getCurrentCountryName();
kr_vpnMode.value =
kr_getConnectionTypeString(KRSingBoxImp().kr_connectionType.value);
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(KRSingBoxImp().kr_connectionType.value);
_kr_getVersion();
}
String kr_getConnectionTypeString(KRConnectionType type) {
switch (type) {
case KRConnectionType.global:
return AppTranslations.kr_setting.connectionTypeGlobal;
case KRConnectionType.rule:
return AppTranslations.kr_setting.connectionTypeRule;
// case KRConnectionType.direct:
// return AppTranslations.kr_setting.connectionTypeDirect;
}
}
String kr_getConnectionTypeRemark(KRConnectionType type) {
switch (type) {
case KRConnectionType.global:
return AppTranslations.kr_setting.connectionTypeGlobalRemark;
case KRConnectionType.rule:
return AppTranslations.kr_setting.connectionTypeRuleRemark;
// case KRConnectionType.direct:
// return AppTranslations.kr_setting.connectionTypeDirectRemark;
}
}
void _loadThemeOption() async {
final KRThemeService themeService = KRThemeService();
await themeService.init();
switch (themeService.kr_Theme) {
case ThemeMode.system:
kr_themeOption.value = AppTranslations.kr_setting.system;
break;
case ThemeMode.light:
kr_themeOption.value = AppTranslations.kr_setting.light;
break;
case ThemeMode.dark:
kr_themeOption.value = AppTranslations.kr_setting.dark;
break;
}
}
final count = 0.obs;
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
void increment() => count.value++;
void kr_updateConnectionType(KRConnectionType newType) {
if (KRSingBoxImp().kr_connectionType.value != newType) {
KRLogUtil.kr_i('更新连接类型: $newType', tag: 'SettingController');
KRSingBoxImp().kr_updateConnectionType(newType);
kr_vpnMode.value = kr_getConnectionTypeString(newType);
kr_vpnModeRemark.value = kr_getConnectionTypeRemark(newType);
// 这里可以添加其他需要的逻辑
}
}
// 获取版本号
Future<void> _kr_getVersion() async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
kr_version.value = packageInfo.version;
}
}
+471
View File
@@ -0,0 +1,471 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
import '../controllers/kr_setting_controller.dart';
import '../../../themes/kr_theme_service.dart';
import '../../../localization/app_translations.dart';
import '../../../routes/app_pages.dart';
class KRSettingView extends GetView<KRSettingController> {
const KRSettingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: Text(
AppTranslations.kr_setting.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
body: Obx(() {
return Container(
height: MediaQuery.of(context).size.height,
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.3],
),
),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: kToolbarHeight + 20.w),
_kr_buildSectionTitle(
context, AppTranslations.kr_setting.vpnConnection),
_kr_buildVPNSection(context),
_kr_buildSectionTitle(
context, AppTranslations.kr_setting.general),
_kr_buildGeneralSection(context),
SizedBox(height: 100.h),
],
),
),
),
);
}),
);
}
Widget _kr_buildSectionTitle(BuildContext context, String title) {
return Padding(
padding: EdgeInsets.fromLTRB(16.w, 24.h, 16.w, 8.h),
child: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
);
}
Widget _kr_buildVPNSection(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
_kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.mode,
value: controller.kr_vpnMode.value,
// subtitle: controller.kr_vpnModeRemark.value,
onTap: () => _kr_showRouteRuleSelectionSheet(context),
),
_kr_buildDivider(),
// _kr_buildSwitchTile(
// context,
// title: AppTranslations.kr_setting.autoConnect,
// value: controller.kr_autoConnect,
// onChanged: (value) => controller.kr_autoConnect.value = value,
// ),
// _kr_buildDivider(),
// _kr_buildSelectionTile(
// context,
// title: AppTranslations.kr_setting.routeRule,
// value: controller.kr_routeRule.value,
// onTap: () => _kr_showRouteRuleSelectionSheet(context),
// ),
// _kr_buildDivider(),
_kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.countrySelector,
subtitle: AppTranslations.kr_setting.connectionTypeRuleRemark,
value: controller.kr_currentCountry.value,
onTap: () => Get.toNamed(Routes.KR_COUNTRY_SELECTOR),
),
],
),
);
}
void _kr_showVPNModeSelectionSheet(BuildContext context) {
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 16.r),
child: Wrap(
children: [
ListTile(
title: Center(
child: Text(AppTranslations.kr_setting.vpnModeSmart),
),
onTap: () {
controller.kr_changeVPNMode(AppTranslations.kr_setting.vpnModeSmart);
Get.back();
},
),
ListTile(
title: Center(
child: Text(AppTranslations.kr_setting.vpnModeSecure),
),
onTap: () {
controller.kr_changeVPNMode(AppTranslations.kr_setting.vpnModeSecure);
Get.back();
},
),
],
),
),
),
);
}
void _kr_showRouteRuleSelectionSheet(BuildContext context) {
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 16.r),
child: Wrap(
children: KRConnectionType.values.map((type) {
return ListTile(
title: Center(
child: Text(
controller.kr_getConnectionTypeString(type),
style: KrAppTextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
onTap: () {
controller.kr_updateConnectionType(type);
Get.back();
},
);
}).toList(),
),
),
),
);
}
Widget _kr_buildGeneralSection(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Obx(() => _kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.appearance,
value: controller.kr_themeOption.value,
onTap: () => _showThemeSelectionSheet(context),
)),
_kr_buildDivider(),
_kr_buildSwitchTile(
context,
title: AppTranslations.kr_setting.notifications,
value: controller.kr_notification,
onChanged: (value) => controller.kr_notification.value = value,
),
_kr_buildDivider(),
_kr_buildSwitchTile(
context,
title: AppTranslations.kr_setting.helpImprove,
value: controller.kr_helpImprove,
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,
),
_kr_buildDivider(),
// _kr_buildTitleTile(
// context,
// title: AppTranslations.kr_setting.rateUs,
// ),
// _kr_buildDivider(),
Obx(() => _kr_buildValueTile(
context,
title: AppTranslations.kr_setting.version,
value: controller.kr_version.value,
)),
_kr_buildDivider(),
_kr_buildSelectionTile(
context,
title: AppTranslations.kr_setting.switchLanguage,
value: controller.kr_language.value,
onTap: controller.kr_changeLanguage,
),
],
),
);
}
void _showThemeSelectionSheet(BuildContext context) {
Get.bottomSheet(
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 16.r),
child: Wrap(
children: ThemeMode.values.map((option) {
String optionText;
switch (option) {
case ThemeMode.system:
optionText = AppTranslations.kr_setting.system;
break;
case ThemeMode.light:
optionText = AppTranslations.kr_setting.light;
break;
case ThemeMode.dark:
optionText = AppTranslations.kr_setting.dark;
break;
}
return ListTile(
title: Center(
child: Text(
optionText,
style: KrAppTextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
onTap: () async {
final KRThemeService themeService = KRThemeService();
await themeService.kr_switchTheme(option);
controller.kr_themeOption.value = optionText;
Get.back();
},
);
}).toList(),
),
),
),
);
}
Widget _kr_buildSelectionTile(
BuildContext context, {
required String title,
required String value,
String? subtitle,
required VoidCallback onTap,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
subtitle: subtitle != null
? Text(
subtitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(width: 4.w),
Icon(
Icons.arrow_forward_ios,
size: 16.r,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
onTap: onTap,
);
}
Widget _kr_buildSwitchTile(
BuildContext context, {
required String title,
String? subtitle,
required RxBool value,
required Function(bool) onChanged,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
subtitle: subtitle != null
? Text(
subtitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
)
: null,
trailing: Obx(
() => CupertinoSwitch(
value: value.value,
onChanged: onChanged,
activeTrackColor: Colors.blue,
),
),
);
}
Widget _kr_buildActionTile(
BuildContext context, {
required String title,
required String trailing,
required VoidCallback onTap,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
trailing: Text(
trailing,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
onTap: onTap,
);
}
Widget _kr_buildTitleTile(
BuildContext context, {
required String title,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
);
}
Widget _kr_buildValueTile(
BuildContext context, {
required String title,
required String value,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
trailing: Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
Widget _kr_buildDivider() {
return Divider(
height: 1.h,
thickness: 0.2,
color: const Color(0xFFEEEEEE),
);
}
}
+11
View File
@@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/kr_splash_controller.dart';
class KRSplashBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRSplashController>(
() => KRSplashController(),
);
}
}
@@ -0,0 +1,131 @@
import 'package:get/get.dart';
import 'dart:io' show Platform;
import 'package:kaer_with_panels/app/utils/kr_network_check.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'dart:async';
class KRSplashController extends GetxController {
// 加载状态
final RxBool kr_isLoading = true.obs;
// 错误状态
final RxBool kr_hasError = false.obs;
// 错误信息
final RxString kr_errorMessage = ''.obs;
// 倒计时
// final count = 0.obs;
// 是否正在加载
final isLoading = true.obs;
// // 是否初始化成功
// final isInitialized = false.obs;
@override
void onInit() {
super.onInit();
_kr_initialize();
}
Future<void> _kr_initialize() async {
try {
// 只在手机端检查网络权限
if (Platform.isIOS || Platform.isAndroid) {
final bool hasNetworkPermission = await KRNetworkCheck.kr_initialize(
Get.context!,
onPermissionGranted: () async {
await _kr_continueInitialization();
},
);
if (!hasNetworkPermission) {
kr_hasError.value = true;
kr_errorMessage.value = AppTranslations.kr_splash.kr_networkPermissionFailed;
return;
}
} else {
// 非手机端直接继续初始化
await _kr_continueInitialization();
}
} catch (e) {
kr_hasError.value = true;
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
}
}
Future<void> _kr_continueInitialization() async {
try {
// 只在手机端检查网络连接
if (Platform.isIOS || Platform.isAndroid) {
final bool isConnected = await KRNetworkCheck.kr_checkNetworkConnection();
if (!isConnected) {
kr_hasError.value = true;
kr_errorMessage.value = AppTranslations.kr_splash.kr_networkConnectionFailed;
return;
}
}
// 初始化配置
await AppConfig().initConfig(
onSuccess: () async {
// 配置初始化成功,继续后续步骤
await _kr_continueAfterConfig();
},
);
} catch (e) {
// 配置初始化失败,显示错误信息
kr_hasError.value = true;
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
}
}
// 配置初始化成功后的后续步骤
Future<void> _kr_continueAfterConfig() async {
try {
// 初始化SingBox
await KRSingBoxImp.instance.init();
// 初始化用户信息
await KRAppRunData.getInstance().kr_initializeUserInfo();
// 等待一小段时间确保所有初始化完成
await Future.delayed(const Duration(milliseconds: 200));
// 验证登录状态是否已正确设置
final loginStatus = KRAppRunData.getInstance().kr_isLogin.value;
KRLogUtil.kr_i('启动完成,最终登录状态: $loginStatus', tag: 'SplashController');
// 直接导航到主页
Get.offAllNamed(Routes.KR_MAIN);
} catch (e) {
// 后续步骤失败,显示错误信息
KRLogUtil.kr_e('启动初始化失败: $e', tag: 'SplashController');
kr_hasError.value = true;
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
}
}
// 重试按钮点击事件
void kr_retry() {
kr_hasError.value = false;
kr_errorMessage.value = '';
_kr_initialize();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:io' show Platform;
import '../controllers/kr_splash_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../widgets/kr_simple_loading.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
class KRSplashView extends GetView<KRSplashController> {
const KRSplashView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bool isMobile = Platform.isIOS || Platform.isAndroid;
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Container(
decoration: isMobile ? 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], // 调整渐变结束位置
),
) : null,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图片区域
KrLocalImage(
imageName: "splash_illustration",
width: 218.w,
height: 194.w,
fit: BoxFit.contain,
),
SizedBox(height: 48.h),
// 标题
Text(
AppTranslations.kr_splash.appName,
style: KrAppTextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
// 副标题
Text(
AppTranslations.kr_splash.slogan,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.sp,
color: theme.textTheme.bodySmall?.color,
height: 1.5,
),
),
SizedBox(height: 24.h),
// 加载指示器或错误信息
Obx(() {
if (controller.kr_hasError.value) {
return Column(
children: [
Text(
controller.kr_errorMessage.value,
style: KrAppTextStyle(
fontSize: 14,
color: theme.textTheme.bodySmall?.color,
),
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: controller.kr_retry,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 32.w,
vertical: 12.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
),
child: Text(AppTranslations.kr_splash.kr_retry),
),
],
);
}
if (controller.kr_isLoading.value) {
return Column(
children: [
KRSimpleLoading(
color: Colors.blue,
size: 24.0,
),
SizedBox(height: 16.h),
Text(
AppTranslations.kr_splash.initializing,
style: KrAppTextStyle(
fontSize: 14,
color: theme.textTheme.bodySmall?.color,
),
),
],
);
}
return const SizedBox.shrink();
}),
],
),
),
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../../kr_home/controllers/kr_home_controller.dart';
import '../controllers/kr_statistics_controller.dart';
class KRStatisticsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRHomeController>(() => KRHomeController());
Get.lazyPut<KRStatisticsController>(() => KRStatisticsController());
}
}
@@ -0,0 +1,229 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart';
import '../../../common/app_run_data.dart';
import '../../../model/response/kr_user_online_duration.dart';
import '../../../modules/kr_home/controllers/kr_home_controller.dart';
import '../../../utils/kr_common_util.dart';
import '../../../utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:easy_refresh/easy_refresh.dart';
class KRStatisticsController extends GetxController {
/// VPN连接状态
final RxString kr_vpnStatus = ''.obs;
/// IP地址
final RxString kr_ipAddress = ''.obs;
/// 连接时间
final RxString kr_connectTime = ''.obs;
/// 协议类型
final RxString kr_protocol = ''.obs;
/// 当前连续记录(天)
final RxInt kr_currentStreak = 0.obs;
/// 最高记录(天)
final RxInt kr_highestStreak = 0.obs;
/// 最长连接时间(天)
final RxInt kr_longestConnection = 0.obs;
/// 每周保护时间数据
final RxList<double> kr_weeklyData = <double>[0, 0, 0, 0, 0, 0, 0].obs;
final RxBool kr_isConnected = false.obs;
late final KRHomeController kr_homeController;
/// 开始时间
final RxInt kr_startTime = 0.obs;
/// 结束时间
final RxInt kr_endTime = 0.obs;
/// 最后连接日期
final RxString kr_lastConnectDate = ''.obs;
final KRUserApi kr_userApi = KRUserApi();
final EasyRefreshController refreshController = EasyRefreshController(
controlFinishRefresh: true,
);
@override
void onInit() {
super.onInit();
kr_homeController = Get.find<KRHomeController>();
_kr_initializeListeners();
_kr_initializeValues();
kr_isConnected.value = kr_homeController.kr_isConnected.value;
ever(kr_homeController.kr_isConnected, (bool connected) {
kr_isConnected.value = connected;
});
ever(KRAppRunData.getInstance().kr_isLogin, (bool isLogin) {
if (!isLogin) {
kr_currentStreak.value = 0;
kr_longestConnection.value = 0;
kr_highestStreak.value = 0;
kr_weeklyData.value = <double>[0, 0, 0, 0, 0, 0, 0];
return;
}
kr_getUserSubscribeTrafficLogs();
});
}
/// 初始化监听器
void _kr_initializeListeners() {
ever(kr_homeController.kr_connectText, _kr_updateVpnStatus);
ever(kr_homeController.kr_currentIp, _kr_updateIpAddress);
ever(kr_homeController.kr_connectionTime, _kr_updateConnectionTime);
ever(kr_homeController.kr_currentProtocol, _kr_updateProtocol);
}
/// 初始化值
void _kr_initializeValues() {
kr_vpnStatus.value = kr_homeController.kr_connectText.value;
kr_ipAddress.value = kr_homeController.kr_currentIp.value;
kr_connectTime.value = kr_homeController.kr_connectionTime.value;
kr_protocol.value = kr_homeController.kr_currentProtocol.value;
// 这里可以添加其他统计数据的初始化
_kr_updateStatistics();
kr_getUserSubscribeTrafficLogs();
}
/// 获取本周的流量日志
Future<void> kr_getUserSubscribeTrafficLogs() async {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
return;
}
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();
final DateTime weekStart = now.subtract(Duration(days: now.weekday - 1));
final DateTime weekStartDate = DateTime(weekStart.year, weekStart.month, weekStart.day);
final DateTime weekEndDate = weekStartDate.add(const Duration(days: 7));
final int startTimestamp = weekStartDate.millisecondsSinceEpoch ~/ 1000;
final int endTimestamp = weekEndDate.millisecondsSinceEpoch ~/ 1000;
// 更新时间戳
kr_startTime.value = startTimestamp;
kr_endTime.value = endTimestamp;
final either = await KRUserApi().kr_getUserOnlineTimeStatistics();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(trafficLogs) {
// 处理流量日志数据
_kr_processTrafficLogs(trafficLogs);
},
);
}
/// 处理流量日志数据
void _kr_processTrafficLogs(KRUserOnlineDurationResponse trafficLogs) {
try {
// 更新连续记录数据
kr_currentStreak.value = trafficLogs.connectionRecords.currentContinuousDays;
kr_highestStreak.value = trafficLogs.connectionRecords.historyContinuousDays;
// 将小时转换为天数并向上取整
kr_longestConnection.value = (trafficLogs.connectionRecords.longestSingleConnection / 24).ceil();
// 更新每周数据
final List<double> weeklyHours = List.filled(7, 0.0);
for (final stat in trafficLogs.weeklyStats) {
if (stat.day >= 1 && stat.day <= 7) {
weeklyHours[stat.day - 1] = stat.hours;
}
}
kr_weeklyData.value = weeklyHours;
} catch (e) {
KRCommonUtil.kr_showToast('处理流量日志数据失败');
KRLogUtil.kr_e('处理流量日志数据失败: $e', tag: 'Statistics');
}
}
void _kr_updateVpnStatus(String value) {
kr_vpnStatus.value = value;
}
void _kr_updateIpAddress(String value) {
kr_ipAddress.value = value.isEmpty ? '0.0.0.0' : value;
}
void _kr_updateConnectionTime(String value) {
kr_connectTime.value = value.isEmpty ? '00:00:00' : value;
}
void _kr_updateProtocol(String value) {
kr_protocol.value = value.isEmpty ? 'UDP' : value;
}
/// 更新统计数据
void _kr_updateStatistics() {
// 不再需要本地更新统计数据,完全依赖接口返回
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
/// 获取根据当前日期调整后的星期标题数组
List<String> kr_getAdjustedWeekTitles() {
final DateTime now = DateTime.now();
final int currentWeekday = now.weekday; // 1-71代表周一,7代表周日
final List<String> weekTitles = [
AppTranslations.kr_statistics.monday,
AppTranslations.kr_statistics.tuesday,
AppTranslations.kr_statistics.wednesday,
AppTranslations.kr_statistics.thursday,
AppTranslations.kr_statistics.friday,
AppTranslations.kr_statistics.saturday,
AppTranslations.kr_statistics.sunday
];
// 重新排序数组,使当前日期对应的星期显示在最后
final List<String> adjustedTitles = [
...weekTitles.sublist(currentWeekday),
...weekTitles.sublist(0, currentWeekday)
];
return adjustedTitles;
}
// 刷新数据
Future<void> kr_onRefresh() async {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
refreshController.finishRefresh();
return;
}
try {
await kr_getUserSubscribeTrafficLogs();
} finally {
refreshController.finishRefresh();
}
}
}
+389
View File
@@ -0,0 +1,389 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_statistics_controller.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:easy_refresh/easy_refresh.dart';
class KRStatisticsView extends GetView<KRStatisticsController> {
const KRStatisticsView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor ,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Align(
alignment: Alignment.centerLeft,
child: Text(
AppTranslations.kr_statistics.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
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: EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
header: DeliveryHeader(
triggerOffset: 50.0,
springRebound: true,
),
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// SizedBox(height: kToolbarHeight + 20.w),
_kr_buildStatusGrid(context),
_kr_buildWeeklyChart(context),
_kr_buildConnectionRecords(context),
],
),
),
),
),
);
}
// 构建状态网格
Widget _kr_buildStatusGrid(BuildContext context) {
// 根据平台调整卡片高度比例 - 优化桌面版本高度
final double aspectRatio = GetPlatform.isDesktop ? 165 / 38 : 165 / 82;
return Container(
padding: EdgeInsets.all(16.r),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 12.h,
crossAxisSpacing: 12.w,
childAspectRatio: aspectRatio, // 使用优化后的高度比例
children: [
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.vpnStatus,
controller.kr_vpnStatus.value,
Icons.link,
isError: true,
vpnStatusColor: controller.kr_vpnStatus.value == '已连接'
? const Color(0xFF67C23A)
: controller.kr_vpnStatus.value == '连接中...'
? const Color(0xFFE6A23C)
: const Color(0xFFF56C6C),
)),
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.ipAddress,
controller.kr_ipAddress.value,
Icons.language,
)),
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.connectionTime,
controller.kr_connectTime.value,
Icons.access_time,
)),
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.protocol,
controller.kr_protocol.value,
Icons.description_outlined,
)),
],
),
);
}
// 构建状态卡片
Widget _kr_buildStatusCard(BuildContext context, String title, String value, IconData icon, {bool isError = false, Color? vpnStatusColor}) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: vpnStatusColor ?? (isError ? Colors.red : Colors.blue),
size: 20.w,
),
SizedBox(width: 8.w),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
title,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
),
],
),
SizedBox(height: 8.w),
Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: vpnStatusColor ?? (isError ? Colors.red : Theme.of(context).textTheme.bodySmall?.color),
),
),
],
),
);
}
// 构建每周图表
Widget _kr_buildWeeklyChart(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.r),
padding: EdgeInsets.fromLTRB(0, 16.r, 16.r, 16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 16.r),
child: Text(
AppTranslations.kr_statistics.weeklyProtectionTime,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 16.w),
SizedBox(
height: 200.w,
child: Obx(() => LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 5,
getTitlesWidget: (value, meta) {
return Padding(
padding: EdgeInsets.only(left: 16.w),
child: Text(
value.toInt().toString(),
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 12,
),
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
reservedSize: 30,
getTitlesWidget: (value, meta) {
final titles = controller.kr_getAdjustedWeekTitles();
int index = value.toInt();
if (index >= 0 && index < titles.length) {
return Text(
titles[index],
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 10,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
return const Text('');
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 6,
minY: 0,
maxY: 20,
lineBarsData: [
LineChartBarData(
spots: controller.kr_weeklyData.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
}).toList(),
isCurved: true,
color: Colors.blue,
barWidth: 2,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.withOpacity(0.2),
Colors.blue.withOpacity(0.05),
],
),
),
),
],
),
)),
),
],
),
);
}
// 构建连接记录
Widget _kr_buildConnectionRecords(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.r),
child: Column(
children: [
Row(
children: [
Expanded(
child: Obx(() => _kr_buildRecordCard(context, AppTranslations.kr_statistics.currentStreak, AppTranslations.kr_statistics.days(controller.kr_currentStreak.value))),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => _kr_buildRecordCard(context, AppTranslations.kr_statistics.highestStreak, AppTranslations.kr_statistics.days(controller.kr_highestStreak.value))),
),
],
),
SizedBox(height: 16.w),
Obx(() => _kr_buildLongestConnection(context)),
],
),
);
}
// 构建记录卡片
Widget _kr_buildRecordCard(BuildContext context, String title, String value) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 8.w),
Text(
value,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
// 构建最长连接时间
Widget _kr_buildLongestConnection(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.access_time,
color: Colors.blue,
size: 24.w,
),
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_statistics.longestConnection,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
Text(
AppTranslations.kr_statistics.days(controller.kr_longestConnection.value),
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_user_info_controller.dart';
class KRUserInfoBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRUserInfoController>(
() => KRUserInfoController(),
);
}
}
@@ -0,0 +1,247 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/mixins/kr_app_bar_opacity_mixin.dart';
import '../../../common/app_config.dart';
import '../../../common/app_run_data.dart';
import '../../../services/api_service/kr_api.user.dart';
import '../../../utils/kr_common_util.dart';
import '../../../utils/kr_event_bus.dart';
import '../../../utils/kr_log_util.dart';
/// 网格项类型枚举
/// 用于区分不同的功能入口类型
enum KRGridItemType {
/// VPN官网入口
vpnWebsite,
/// 推特社交入口
telegram,
/// 邮箱联系入口
mail,
/// 电话联系入口
phone,
/// 人工客服支持入口
customerService,
/// 客服人员联系入口
contactService,
}
/// 网格项数据模型
/// 用于统一管理功能入口的展示数据
class KRGridItem {
/// 功能图标
final String icon;
/// 功能标题
final String title;
/// 功能描述
final String subtitle;
/// 功能类型
final KRGridItemType type;
const KRGridItem({
required this.icon,
required this.title,
required this.subtitle,
required this.type,
});
}
/// 用户信息页面的控制器
/// 负责管理用户信息页面的状态和业务逻辑
class KRUserInfoController extends GetxController with KRAppBarOpacityMixin {
/// 广告拦截开关状态
/// true: 开启拦截, false: 关闭拦截
final RxBool kr_isAdBlockEnabled = true.obs;
/// NDS解锁开关状态
/// true: 已解锁, false: 未解锁
final RxBool kr_isNDSUnlockEnabled = true.obs;
/// 订阅状态
/// true: 有效订阅, false: 无效订阅
final RxBool kr_hasValidSubscription = false.obs;
/// 是否显示绑定提示
final RxBool kr_showBindingTip = false.obs;
/// 用户余额
RxDouble kr_balance = 0.0.obs;
/// 功能入口网格项列表
/// 包含所有可用的功能入口配置
final kr_gridItems = <KRGridItem>[
KRGridItem(
icon: "my_net_index",
title: AppTranslations.kr_userInfo.vpnWebsite,
subtitle: AppConfig.getInstance().kr_official_website,
type: KRGridItemType.vpnWebsite,
),
KRGridItem(
icon: "my_telegram",
title: AppTranslations.kr_userInfo.telegram,
subtitle: "telegram",
type: KRGridItemType.telegram,
),
KRGridItem(
icon: "my_email",
title: AppTranslations.kr_userInfo.mail,
subtitle: AppConfig.getInstance().kr_official_email,
type: KRGridItemType.mail,
),
KRGridItem(
icon: "my_phone",
title: AppTranslations.kr_userInfo.phone,
subtitle: AppConfig.getInstance().kr_official_telephone,
type: KRGridItemType.phone,
),
KRGridItem(
icon: "my_kf",
title: AppTranslations.kr_userInfo.customerService,
subtitle: "",
type: KRGridItemType.customerService,
),
KRGridItem(
icon: "my_kf_msg",
title: AppTranslations.kr_userInfo.contactService,
subtitle: "",
type: KRGridItemType.contactService,
),
].obs;
@override
void onInit() {
super.onInit();
kr_initData();
}
/// 页面进入时的处理
void kr_onPageEnter() {
KRLogUtil.kr_i('进入用户信息页面', tag: 'UserInfo');
_loadUserInfo();
}
@override
void onReady() {
super.onReady();
// 每次进入页面时执行
KRLogUtil.kr_i('进入用户信息页面', tag: 'UserInfo');
// 刷新用户信息
_loadUserInfo();
}
@override
void onClose() {
super.onClose();
}
/// 初始化控制器数据
/// 从服务器获取用户配置信息
void kr_initData() {
ever(KRAppRunData.getInstance().kr_isLogin, (bool isLogin) {
if (isLogin) {
_loadUserInfo();
} else {
kr_balance.value = 0.0;
}
});
ever(KRSingBoxImp().kr_blockAds, (bool bl) {
kr_isAdBlockEnabled.value = bl;
});
kr_isAdBlockEnabled.value = KRSingBoxImp().kr_blockAds.value;
// 监听所有支付相关消息
KREventBus().kr_listenMessages(
[KRMessageType.kr_payment, KRMessageType.kr_subscribe_update],
_kr_handleMessage,
);
}
/// 处理消息
void _kr_handleMessage(KRMessageData message) {
switch (message.kr_type) {
case KRMessageType.kr_payment:
_loadUserInfo();
break;
case KRMessageType.kr_subscribe_update:
break;
// TODO: Handle this case.
}
}
/// 处理广告拦截开关状态变化
/// [value] 新的开关状态
void kr_toggleAdBlock(bool value) {
kr_isAdBlockEnabled.value = value;
KRSingBoxImp().kr_updateAdBlockEnabled(value);
}
/// 处理NDS解锁开关状态变化
/// [value] 新的开关状态
void kr_toggleNDSUnlock(bool value) {
kr_isNDSUnlockEnabled.value = value;
// TODO: 实现保存设置到服务器的逻辑
}
/// 初始化用户信息
Future<void> _loadUserInfo() async {
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;
},
);
}
/// 处理用户退出登录
/// 清理用户数据并返回登录页面
void kr_handleLogout() {
KRAppRunData.getInstance().kr_loginOut();
}
/// 重置流量使用量
/// 调用服务器API重置用户的流量使用量
Future<void> kr_resetTraffic() async {
try {
// TODO: 调用服务器API重置流量
KRCommonUtil.kr_showToast(
AppTranslations.kr_userInfo.resetTrafficSuccess);
} catch (e) {
KRCommonUtil.kr_showToast(AppTranslations.kr_userInfo.resetTrafficFailed);
}
}
String getTitle(KRGridItemType type) {
switch (type) {
case KRGridItemType.vpnWebsite:
return AppTranslations.kr_userInfo.vpnWebsite;
case KRGridItemType.telegram:
return AppTranslations.kr_userInfo.telegram;
case KRGridItemType.mail:
return AppTranslations.kr_userInfo.mail;
case KRGridItemType.phone:
return AppTranslations.kr_userInfo.phone;
case KRGridItemType.customerService:
return AppTranslations.kr_userInfo.customerService;
case KRGridItemType.contactService:
return AppTranslations.kr_userInfo.contactService;
}
}
}
+897
View File
@@ -0,0 +1,897 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/services.dart';
import 'dart:io' show Platform;
import 'package:kaer_with_panels/app/routes/app_pages.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/modules/kr_main/controllers/kr_main_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_home/widgets/kr_subscribe_selector_view.dart';
import '../../../common/app_run_data.dart';
import '../../../model/response/kr_user_available_subscribe.dart';
import '../../../services/kr_subscribe_service.dart';
import '../../../widgets/dialogs/kr_dialog.dart';
import '../controllers/kr_user_info_controller.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
class KRUserInfoView extends GetView<KRUserInfoController> {
const KRUserInfoView({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,
title: Align(
alignment: Alignment.centerLeft,
child: Text(
AppTranslations.kr_userInfo.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
actions: [
Padding(
padding: EdgeInsets.only(right: 8.w),
child: IconButton(
icon: Icon(
Icons.settings,
color: Theme.of(context).iconTheme.color,
size: 22.w,
),
onPressed: () => Get.toNamed(Routes.KR_SETTING),
),
),
],
),
Expanded(
child: Stack(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_kr_buildBindingTip(context),
_kr_buildSubscriptionCard(context),
_kr_buildShortcutSection(context),
_kr_buildOtherSection(context),
_kr_buildLogoutButton(context),
SizedBox(height: 30.w),
],
),
),
],
),
),
],
),
),
);
}
// 构建绑定提示
Widget _kr_buildBindingTip(BuildContext context) {
return Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
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,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
KRAppRunData.getInstance().kr_isLogin.value
? "${AppTranslations.kr_userInfo.myAccount} ${KRAppRunData().kr_account}"
: AppTranslations.kr_userInfo.bindingTip,
style: KrAppTextStyle(
color: !KRAppRunData.getInstance().kr_isLogin.value
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 12,
),
),
],
),
),
// 余额信息(写死预览)
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,
),
),
],
),
),
),
],
));
}
// 构建订阅卡片
Widget _kr_buildSubscriptionCard(BuildContext context) {
return Obx(() {
final isLoggedIn = KRAppRunData.getInstance().kr_isLogin.value;
final subscribe = KRSubscribeService().kr_currentSubscribe.value;
if (isLoggedIn && subscribe != null) {
return _kr_buildValidSubscriptionCard(context, subscribe);
} else {
return _kr_buildInvalidSubscriptionCard(context, isLoggedIn);
}
});
}
// 构建有效订阅卡片
Widget _kr_buildValidSubscriptionCard(
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
final bool isExpired =
DateTime.parse(subscribe.expireTime).isBefore(DateTime.now());
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
padding: EdgeInsets.only(
left: 16.w, right: 16.w, top: 16.w, bottom: AppConfig().kr_is_daytime == false ? 0 : 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: [
Expanded(
child: Text(
subscribe.name,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
GestureDetector(
onTap: () {
if (KRSubscribeService().kr_currentStatus.value ==
KRSubscribeServiceStatus.kr_loading) {
return;
}
final homeController = Get.find<KRHomeController>();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
child: KRSubscribeSelectorView(
controller: homeController,
),
),
);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppTranslations.kr_userInfo.switchSubscription,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(width: 4.w),
Icon(
Icons.swap_horiz,
color: Theme.of(context).textTheme.bodySmall?.color,
size: 16.w,
),
],
),
),
),
],
),
SizedBox(height: 16.w),
// 过期时间
Row(
children: [
Icon(
isExpired ? Icons.warning_amber_rounded : Icons.check_circle,
color: isExpired
? Theme.of(context).colorScheme.error
: Colors.green,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
"${AppTranslations.kr_userInfo.expireTime}${subscribe.expireTime}",
style: KrAppTextStyle(
fontSize: 12,
color: isExpired
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
SizedBox(height: 16.w),
// 流量进度条
_kr_buildTrafficProgress(context, subscribe),
SizedBox(height: 16.w),
// 操作按钮
_kr_buildSubscriptionActions(context, subscribe),
],
),
);
}
// 构建流量进度条
Widget _kr_buildTrafficProgress(
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
final int totalTraffic = subscribe.traffic;
final int usedTraffic = subscribe.download + subscribe.upload;
// 模拟流量超出
var progress = totalTraffic > 0 ? usedTraffic / totalTraffic.toDouble() : 0;
KRLogUtil.kr_i(
"progress: ${AppTranslations.kr_userInfo.deviceLimit.trParams({
'count': subscribe.deviceLimit.toString()
})}",
tag: "KRUserInfoView");
final bool isTrafficExceeded = progress >= 1; // 模拟流量超出
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.info_outline,
size: 16.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
SizedBox(width: 4.w),
Text(
AppTranslations.kr_userInfo.deviceLimit
.trParams({'count': subscribe.deviceLimit.toString()}),
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
Row(
children: [
if (isTrafficExceeded) ...[
Icon(
Icons.warning_amber_rounded,
size: 14.w,
color: Theme.of(context).colorScheme.error,
),
SizedBox(width: 4.w),
],
LayoutBuilder(
builder: (context, constraints) {
final text = totalTraffic == 0
? AppTranslations.kr_userInfo.trafficProgressUnlimited
: isTrafficExceeded
? KRCommonUtil.kr_formatBytes(usedTraffic)
: "${KRCommonUtil.kr_formatBytes(usedTraffic)} / ${KRCommonUtil.kr_formatBytes(totalTraffic)}";
// 根据文本长度和可用宽度计算合适的字体大小
final baseFontSize = 12.0;
final textLength = text.length;
final availableWidth = constraints.maxWidth;
final calculatedFontSize =
(availableWidth / (textLength * 0.8))
.clamp(8.0, baseFontSize);
return Text(
text,
style: KrAppTextStyle(
fontSize: calculatedFontSize,
color: isTrafficExceeded
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodySmall?.color,
),
);
},
),
if (isTrafficExceeded) ...[
SizedBox(width: 8.w),
GestureDetector(
onTap: () {
final currentExpireTime =
DateTime.parse(subscribe.expireTime);
final newExpireTime =
currentExpireTime.subtract(const Duration(days: 30));
KRDialog.show(
title: AppTranslations.kr_userInfo.resetTrafficTitle,
message:
AppTranslations.kr_userInfo.resetTrafficMessage(
currentExpireTime.toString().split(' ')[0],
newExpireTime.toString().split(' ')[0],
),
cancelText: AppTranslations.kr_dialog.kr_cancel,
confirmText: AppTranslations.kr_dialog.kr_confirm,
onCancel: () => Get.back(),
onConfirm: () =>
KRSubscribeService().kr_resetSubscribePeriod(),
);
},
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.w),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.error
.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 14.w,
color: Theme.of(context).colorScheme.error,
),
SizedBox(width: 4.w),
Text(
AppTranslations.kr_userInfo.reset.tr,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
),
],
),
),
),
],
],
),
],
),
if (totalTraffic > 0) ...[
SizedBox(height: 6.w),
Stack(
children: [
Container(
height: 6.w,
decoration: BoxDecoration(
color: Theme.of(context).dividerColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(3.w),
),
),
Container(
height: 6.w,
width: MediaQuery.of(context).size.width * progress,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: progress > 0.8
? [
const Color(0xFFFF6B6B), // 浅红色
const Color(0xFFFF4757), // 深红色
]
: [
const Color(0xFF00C6FF), // 亮蓝色
const Color(0xFF0072FF), // 深蓝色
],
),
borderRadius: BorderRadius.circular(3.w),
boxShadow: [
BoxShadow(
color: (progress > 0.8
? const Color(0xFFFF4757)
: const Color(0xFF0072FF))
.withOpacity(0.3),
blurRadius: 4.w,
offset: Offset(0, 2.w),
),
],
),
),
],
),
],
],
);
}
// 构建订阅操作按钮
Widget _kr_buildSubscriptionActions(
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
if (!AppConfig.getInstance().kr_is_daytime) {
return SizedBox();
}
return Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1797FF),
foregroundColor: Colors.white,
elevation: 0,
padding: EdgeInsets.symmetric(vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.w),
),
),
child: Text(
AppTranslations.kr_userInfo.subscribeNow,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
);
}
// 构建无效订阅卡片
Widget _kr_buildInvalidSubscriptionCard(
BuildContext context, bool isLoggedIn) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.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: Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: Theme.of(context).colorScheme.error,
size: 16.w,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
!isLoggedIn
? AppTranslations.kr_userInfo.pleaseLogin
: AppTranslations.kr_userInfo.noValidSubscription,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
SizedBox(width: 12.w),
ElevatedButton(
onPressed: !isLoggedIn
? () => Get.find<KRMainController>().kr_setPage(0)
: () => Get.toNamed(Routes.KR_PURCHASE_MEMBERSHIP),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1797FF),
foregroundColor: Colors.white,
elevation: 0,
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.w),
),
),
child: Text(
!isLoggedIn
? AppTranslations.kr_userInfo.loginNow
: AppTranslations.kr_userInfo.subscribeNow,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
],
),
);
}
// 构建快捷键区域
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,
),
),
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,
),
],
),
],
),
);
}
// 构建快捷键容器
Widget _kr_buildShortcutContainer({
required String icon,
required String title,
RxBool? value,
Function(bool)? onChanged,
VoidCallback? onTap,
required BuildContext context,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 62.w,
margin: EdgeInsets.symmetric(vertical: 6.w),
padding: EdgeInsets.only(left: 12.w, right: 12.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
KrLocalImage(
imageName: icon,
color: Theme.of(context).textTheme.bodyMedium?.color,
width: 40.w,
height: 40.w,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
),
if (value != null)
Obx(
() => CupertinoSwitch(
value: value.value,
onChanged: onChanged,
activeColor: Colors.blue,
),
)
else
Icon(
Icons.arrow_forward_ios,
color: Theme.of(context).textTheme.bodySmall?.color,
size: 16.w,
),
],
),
),
);
}
// 构建其他区域
Widget _kr_buildOtherSection(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_userInfo.others,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 12.w),
Obx(() => Wrap(
spacing: 12.w,
runSpacing: 12.w,
children: List.generate(
controller.kr_gridItems.length,
(index) => SizedBox(
width: (MediaQuery.of(context).size.width - 44.w) /
2, // 44.w = 左右margin(32.w) + 中间间距(12.w)
child: _kr_buildGridItem(
controller.kr_gridItems[index], index, context),
),
),
)),
],
),
);
}
// 构建网格项
Widget _kr_buildGridItem(KRGridItem item, int index, BuildContext context) {
return InkWell(
onTap: () => _kr_handleGridItemTap(item.type),
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
KrLocalImage(
imageName: item.icon,
width: 24.w,
height: 24.w,
),
KrLocalImage(
imageName: "my_et",
color: Theme.of(context).textTheme.bodySmall?.color,
width: 16.w,
height: 16.w,
),
],
),
SizedBox(height: 8.w),
Text(
controller.getTitle(item.type),
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
SizedBox(height: 4.w),
Text(
item.subtitle,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
);
}
// 构建退出登录按钮
Widget _kr_buildLogoutButton(BuildContext context) {
return Obx(() => Visibility(
visible: KRAppRunData.getInstance().kr_isLogin.value,
child: Container(
width: double.infinity,
margin: EdgeInsets.all(16.w),
child: TextButton(
onPressed: () {
KRDialog.show(
title: AppTranslations.kr_userInfo.logoutConfirmTitle,
message: AppTranslations.kr_userInfo.logoutConfirmMessage,
cancelText: AppTranslations.kr_userInfo.logoutCancel,
onCancel: () => Get.back(),
onConfirm: () => controller.kr_handleLogout(),
);
},
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).cardColor,
padding: EdgeInsets.symmetric(vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.w),
),
),
child: Text(
AppTranslations.kr_userInfo.logout,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
),
));
}
// 处理网格项点击
Future<void> _kr_handleGridItemTap(KRGridItemType type) async {
switch (type) {
case KRGridItemType.vpnWebsite:
final Uri url = Uri.parse(AppConfig.getInstance().kr_official_website);
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
break;
case KRGridItemType.telegram:
final String tgUrl = AppConfig.getInstance().kr_official_telegram;
final String inviteCode = tgUrl.split('/').last.replaceAll('+', '');
if (Platform.isAndroid || Platform.isIOS) {
// 尝试多种 URL Scheme
final List<String> schemes = [
'tg://join?invite=$inviteCode', // Android 主要格式
'telegram://join?invite=$inviteCode', // iOS 可能使用的格式
];
bool launched = false;
for (String scheme in schemes) {
try {
final Uri tgAppUrl = Uri.parse(scheme);
if (await canLaunchUrl(tgAppUrl)) {
await launchUrl(tgAppUrl);
launched = true;
break;
}
} catch (e) {
continue;
}
}
if (!launched) {
KRCommonUtil.kr_showToast("尝试使用浏览器打开");
// 降级使用浏览器打开
try {
final Uri webUrl = Uri.parse(tgUrl);
if (await canLaunchUrl(webUrl)) {
await launchUrl(webUrl, mode: LaunchMode.externalApplication);
} else {
KRCommonUtil.kr_showToast("无法打开浏览器");
}
} catch (e) {
KRCommonUtil.kr_showToast("打开链接失败,请稍后重试");
}
}
} else {
// 桌面端处理
try {
final Uri webUrl = Uri.parse(tgUrl);
if (await canLaunchUrl(webUrl)) {
await launchUrl(webUrl);
} else {
KRCommonUtil.kr_showToast("无法打开Telegram链接");
}
} catch (e) {
KRCommonUtil.kr_showToast("打开链接失败,请稍后重试");
}
}
break;
case KRGridItemType.mail:
final String email = AppConfig.getInstance().kr_official_email;
await Clipboard.setData(ClipboardData(text: email));
KRCommonUtil.kr_showToast(AppTranslations.kr_userInfo.copySuccess);
break;
case KRGridItemType.phone:
final String phone = AppConfig.getInstance().kr_official_telephone;
if (Platform.isAndroid || Platform.isIOS) {
final Uri phoneUri = Uri.parse('tel:$phone');
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
} else {
KRCommonUtil.kr_showToast(AppTranslations.kr_userInfo.notAvailable);
}
} else {
await Clipboard.setData(ClipboardData(text: phone));
KRCommonUtil.kr_showToast(AppTranslations.kr_userInfo.copySuccess);
}
break;
case KRGridItemType.customerService:
Get.toNamed(Routes.KR_CRISP);
break;
case KRGridItemType.contactService:
Get.toNamed(Routes.KR_CRISP);
break;
}
}
}
@@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/kr_webview_controller.dart';
class KRWebViewBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRWebViewController>(
() => KRWebViewController(),
);
}
}
@@ -0,0 +1,192 @@
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_web_api.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
/// WebView 控制器
/// 用于管理 WebView 的状态和行为
class KRWebViewController extends GetxController {
// 页面加载状态
final RxBool kr_isLoading = true.obs;
// 页面标题
final RxString kr_title = ''.obs;
// WebView 控制器
late final WebViewController kr_webViewController;
// 默认URL
static const String kr_defaultUrl = '';
final String kr_url = Get.arguments['url'] as String;
// Web API 实例
final KRWebApi _kr_webApi = KRWebApi();
// 内容类型
final RxBool kr_isHtml = false.obs;
final RxBool kr_isMarkdown = false.obs;
final RxString kr_content = ''.obs;
@override
void onInit() {
super.onInit();
// 根据 URL 类型决定初始化方式
if (kr_url.contains(Api.kr_getSiteTos) || kr_url.contains(Api.kr_getSitePrivacy)) {
// 用户协议和隐私政策页面,直接获取文本内容
if (kr_url.contains(Api.kr_getSiteTos)) {
kr_title.value = AppTranslations.kr_login.termsOfService;
} else {
kr_title.value = AppTranslations.kr_login.privacyPolicy;
}
kr_getWebText();
} else {
// 其他页面,初始化 WebView
kr_initWebView();
}
}
/// 初始化 WebView
void kr_initWebView() {
kr_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) async {
// 只在移动平台处理支付应用跳转
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
KRLogUtil.kr_i('处理支付链接: ${request.url}', tag: 'WebViewController');
// 处理支付链接
if (await kr_handleUrlLaunch(request.url)) {
return NavigationDecision.prevent;
}
}
return NavigationDecision.navigate;
},
onPageStarted: kr_handlePageStarted,
onPageFinished: kr_handlePageFinished,
),
);
// 检查是否是用户协议或隐私政策
if (kr_url.contains(Api.kr_getSiteTos) || kr_url.contains(Api.kr_getSitePrivacy)) {
kr_getWebText();
} else {
kr_webViewController.loadRequest(Uri.parse(kr_url));
}
}
/// 获取网页文本内容并加载到 WebView
Future<void> kr_getWebText() async {
try {
final response = await _kr_webApi.kr_getWebText(kr_url);
response.fold(
(error) async {
KRLogUtil.kr_e('获取网页内容失败: $error', tag: 'WebViewController');
// 如果获取失败,直接设置错误内容
kr_content.value = 'Failed to load, please try again later';
kr_isLoading.value = false;
},
(content) async {
KRLogUtil.kr_i('获取到内容: $content', tag: 'WebViewController');
// 判断内容类型,优先判断 Markdown
kr_isMarkdown.value = content.contains('**') ||
content.contains('*') ||
content.contains('#') ||
content.contains('- ') ||
content.contains('[');
kr_isHtml.value = !kr_isMarkdown.value && content.contains('<') && content.contains('>');
KRLogUtil.kr_i('内容类型 - Markdown: ${kr_isMarkdown.value}, HTML: ${kr_isHtml.value}', tag: 'WebViewController');
if (kr_isMarkdown.value) {
// 如果是 Markdown 内容,直接使用
kr_content.value = content;
} else if (kr_isHtml.value) {
// 如果是 HTML 内容,直接使用
kr_content.value = content;
} else {
// 如果是普通文本,直接使用
kr_content.value = content;
}
kr_isLoading.value = false;
},
);
} catch (e) {
KRLogUtil.kr_e('获取网页内容出错: $e', tag: 'WebViewController');
kr_content.value = 'Loading error, please try again later';
kr_isLoading.value = false;
}
}
/// 处理页面开始加载事件
void kr_handlePageStarted(String url) {
kr_isLoading.value = true;
}
/// 处理页面加载完成事件
void kr_handlePageFinished(String url) async {
kr_isLoading.value = false;
await kr_updateTitle();
}
/// 更新页面标题
Future<void> kr_updateTitle() async {
final String? kr_pageTitle = await kr_webViewController.getTitle();
kr_title.value = kr_pageTitle ?? '';
}
/// 重新加载页面
Future<void> kr_reloadPage() async {
await kr_webViewController.reload();
}
/// 加载新的URL
Future<void> kr_loadUrl(String url) async {
await kr_webViewController.loadRequest(Uri.parse(url));
}
/// 处理URL启动
Future<bool> kr_handleUrlLaunch(String url) async {
try {
KRLogUtil.kr_i('正在处理URL跳转: $url', tag: 'WebViewController');
final uri = Uri.parse(url);
// 处理支付应用和外部链接
if (uri.scheme == 'alipays' ||
uri.scheme == 'alipay' ||
uri.scheme == 'weixin' ||
uri.scheme == 'wx') {
KRLogUtil.kr_i('检测到支付应用scheme: ${uri.scheme}', tag: 'WebViewController');
// 尝试打开支付应用
if (await canLaunchUrl(uri)) {
return await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
}
// 如果支付应用无法打开,尝试使用外部浏览器打开
final httpUri = Uri.parse('https://${uri.host}${uri.path}?${uri.query}');
KRLogUtil.kr_i('尝试使用浏览器打开: $httpUri', tag: 'WebViewController');
if (await canLaunchUrl(httpUri)) {
return await launchUrl(
httpUri,
mode: LaunchMode.externalApplication,
);
}
KRLogUtil.kr_e('无法启动URL: $url', tag: 'WebViewController');
}
return false;
} catch (e) {
KRLogUtil.kr_e('URL跳转错误: $e', tag: 'WebViewController');
return false;
}
}
}
+177
View File
@@ -0,0 +1,177 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../widgets/kr_app_text_style.dart';
import '../controllers/kr_webview_controller.dart';
import '../../../services/api_service/api.dart';
import '../../../utils/kr_log_util.dart';
/// WebView 页面组件
class KRWebView extends GetView<KRWebViewController> {
const KRWebView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.sp,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: Text(
controller.kr_title.value,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
body: _buildBody(),
);
}
/// 构建主体内容
Widget _buildBody() {
return Stack(
children: [
_buildContent(),
_buildLoadingIndicator(),
],
);
}
/// 构建内容组件
Widget _buildContent() {
if (controller.kr_url.contains(Api.kr_getSiteTos) ||
controller.kr_url.contains(Api.kr_getSitePrivacy)) {
return _buildProtocolContent();
} else {
return _buildWebView();
}
}
/// 构建协议内容
Widget _buildProtocolContent() {
return Obx(() {
if (controller.kr_isHtml.value) {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Html(
data: controller.kr_content.value,
style: {
'body': Style(
margin: Margins.all(0),
padding: HtmlPaddings.all(0),
fontSize: FontSize(14.sp),
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
lineHeight: LineHeight(1.4),
),
'p': Style(
margin: Margins.only(bottom: 8.h),
),
'b': Style(
fontWeight: FontWeight.bold,
),
'i': Style(
fontStyle: FontStyle.italic,
),
'a': Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
},
shrinkWrap: true,
),
);
} else if (controller.kr_isMarkdown.value) {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: MarkdownBody(
data: controller.kr_content.value,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: 14.sp,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
strong: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
em: TextStyle(
fontSize: 14.sp,
fontStyle: FontStyle.italic,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
a: TextStyle(
fontSize: 14.sp,
color: Colors.blue,
decoration: TextDecoration.underline,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
),
),
);
} else {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Text(
controller.kr_content.value,
style: TextStyle(
fontSize: 14.sp,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
),
);
}
});
}
/// 构建 WebView 组件
Widget _buildWebView() {
return WebViewWidget(
controller: controller.kr_webViewController,
);
}
/// 构建加载指示器
Widget _buildLoadingIndicator() {
return Obx(
() => controller.kr_isLoading.value
? const Center(
child: CircularProgressIndicator(),
)
: const SizedBox.shrink(),
);
}
/// 显示错误提示
void _showErrorSnackbar(String title, String message) {
Get.snackbar(
title,
message,
snackPosition: SnackPosition.BOTTOM,
);
}
}