初始化提交

This commit is contained in:
2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions
+1194
View File
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
import 'dart:convert';
import 'dart:async';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'package:kaer_with_panels/app/modules/kr_main/controllers/kr_main_controller.dart';
import 'package:kaer_with_panels/app/services/kr_socket_service.dart';
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../services/api_service/kr_api.user.dart';
import '../utils/kr_event_bus.dart';
class KRAppRunData {
static final KRAppRunData _instance = KRAppRunData._internal();
static const String _keyUserInfo = 'USER_INFO';
/// 登录token
String? kr_token;
/// 用户账号
String? kr_account;
/// 用户ID
String? kr_userId;
/// 登录类型
KRLoginType? kr_loginType;
/// 区号
String? kr_areaCode;
// 需要被监听的属性,用 obs 包装
final kr_isLogin = false.obs;
KRAppRunData._internal();
factory KRAppRunData() => _instance;
static KRAppRunData getInstance() {
return _instance;
}
/// 保存用户信息
Future<void> kr_saveUserInfo(
String token, String account, KRLoginType loginType, String? areaCode) async {
KRLogUtil.kr_i('开始保存用户信息', tag: 'AppRunData');
try {
// 更新内存中的数据
kr_token = token;
kr_account = account;
kr_loginType = loginType;
kr_areaCode = areaCode;
final Map<String, dynamic> userInfo = {
'token': token,
'account': account,
'loginType': loginType.value,
'areaCode': areaCode ?? "",
};
KRLogUtil.kr_i('准备保存用户信息到存储', tag: 'AppRunData');
await KRSecureStorage().kr_saveData(
key: _keyUserInfo,
value: jsonEncode(userInfo),
);
// 验证保存是否成功
final savedData = await KRSecureStorage().kr_readData(key: _keyUserInfo);
if (savedData == null || savedData.isEmpty) {
KRLogUtil.kr_e('数据保存后无法读取,保存失败', tag: 'AppRunData');
kr_isLogin.value = false;
return;
}
KRLogUtil.kr_i('用户信息保存成功,设置登录状态为true', tag: 'AppRunData');
// 只有在保存成功后才设置登录状态
kr_isLogin.value = true;
// 异步获取用户信息并建立 Socket 连接,不等待结果
_iniUserInfo().catchError((error) {
KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData');
// 即使获取用户信息失败,也保持登录状态
});
} catch (e) {
KRLogUtil.kr_e('保存用户信息失败: $e', tag: 'AppRunData');
// 如果出错,重置登录状态
kr_isLogin.value = false;
rethrow; // 重新抛出异常,让调用者知道保存失败
}
}
/// 退出登录
Future<void> kr_loginOut() async {
// 先将登录状态设置为 false,防止重连
kr_isLogin.value = false;
// 断开 Socket 连接
await _kr_disconnectSocket();
// 清理用户信息
kr_token = null;
kr_account = null;
kr_userId = null;
kr_loginType = null;
kr_areaCode = null;
// 删除存储的用户信息
await KRSecureStorage().kr_deleteData(key: _keyUserInfo);
// 重置主页面
Get.find<KRMainController>().kr_setPage(0);
}
/// 初始化用户信息
Future<void> kr_initializeUserInfo() async {
KRLogUtil.kr_i('开始初始化用户信息', tag: 'AppRunData');
try {
final String? userInfoString =
await KRSecureStorage().kr_readData(key: _keyUserInfo);
if (userInfoString != null && userInfoString.isNotEmpty) {
KRLogUtil.kr_i('找到存储的用户信息,开始解析', tag: 'AppRunData');
try {
final Map<String, dynamic> userInfo = jsonDecode(userInfoString);
kr_token = userInfo['token'];
kr_account = userInfo['account'];
final loginTypeValue = userInfo['loginType'];
kr_loginType = KRLoginType.values.firstWhere(
(e) => e.value == loginTypeValue,
orElse: () => KRLoginType.kr_telephone,
);
kr_areaCode = userInfo['areaCode'] ?? "";
KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=$kr_account', tag: 'AppRunData');
// 验证token有效性
if (kr_token != null && kr_token!.isNotEmpty) {
KRLogUtil.kr_i('设置登录状态为true', tag: 'AppRunData');
kr_isLogin.value = true;
// 异步获取用户信息,但不等待结果
_iniUserInfo().catchError((error) {
KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData');
// 如果获取用户信息失败,不重置登录状态,让用户重试
});
} else {
KRLogUtil.kr_w('Token为空,设置为未登录状态', tag: 'AppRunData');
kr_isLogin.value = false;
}
} catch (e) {
KRLogUtil.kr_e('解析用户信息失败: $e', tag: 'AppRunData');
await kr_loginOut();
}
} else {
KRLogUtil.kr_i('未找到存储的用户信息,设置为未登录状态', tag: 'AppRunData');
kr_isLogin.value = false;
}
} catch (e) {
KRLogUtil.kr_e('初始化用户信息过程出错: $e', tag: 'AppRunData');
kr_isLogin.value = false;
}
KRLogUtil.kr_i('用户信息初始化完成,登录状态: ${kr_isLogin.value}', tag: 'AppRunData');
}
/// 初始化用户信息并建立 Socket 连接
Future<void> _iniUserInfo() async {
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
kr_userId = userInfo.id.toString();
_kr_connectSocket(kr_userId!);
},
);
}
/// 建立 Socket 连接
Future<void> _kr_connectSocket(String userId) async {
// 如果已存在连接,先断开
await _kr_disconnectSocket();
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'AppRunData');
KrSocketService.instance.kr_init(
baseUrl: AppConfig.getInstance().wsBaseUrl,
userId: userId,
deviceNumber: deviceId,
token: kr_token ?? "",
);
// 设置消息处理回调
KrSocketService.instance.setOnMessageCallback(_kr_handleMessage);
// 设置连接状态回调
KrSocketService.instance.setOnConnectionStateCallback(_kr_handleConnectionState);
// 建立连接
KrSocketService.instance.connect();
}
/// 处理接收到的消息
void _kr_handleMessage(Map<String, dynamic> message) {
try {
final String method = message['method'] as String;
switch (method) {
case 'kicked_device':
KRLogUtil.kr_i('超出登录设备限制', tag: 'AppRunData');
kr_loginOut();
break;
case 'kicked_admin':
KRLogUtil.kr_i('强制退出', tag: 'AppRunData');
kr_loginOut();
break;
case 'subscribe_update':
KRLogUtil.kr_i('订阅信息已更新', tag: 'AppRunData');
// 发送订阅更新事件
KREventBus().kr_sendMessage(KRMessageType.kr_subscribe_update);
break;
default:
KRLogUtil.kr_w('收到未知类型的消息: $message', tag: 'AppRunData');
}
} catch (e) {
KRLogUtil.kr_e('处理消息失败: $e', tag: 'AppRunData');
}
}
/// 处理连接状态变化
void _kr_handleConnectionState(bool isConnected) {
KRLogUtil.kr_i('WebSocket 连接状态: ${isConnected ? "已连接" : "已断开"}', tag: 'AppRunData');
}
/// 断开 Socket 连接
Future<void> _kr_disconnectSocket() async {
await KrSocketService.instance.disconnect();
}
}
+1
View File
@@ -0,0 +1 @@
+25
View File
@@ -0,0 +1,25 @@
class KRInviteProgress {
final int pending; // 待下载
final int processing; // 在路上
final int success; // 已成功
final int expired; // 已失效
final String? referCode; // 邀请码
KRInviteProgress({
this.pending = 0,
this.processing = 0,
this.success = 0,
this.expired = 0,
this.referCode,
});
factory KRInviteProgress.fromJson(Map<String, dynamic> json) {
return KRInviteProgress(
pending: json['pending'] ?? 0,
processing: json['processing'] ?? 0,
success: json['success'] ?? 0,
expired: json['expired'] ?? 0,
referCode: json['referCode'],
);
}
}
+931
View File
@@ -0,0 +1,931 @@
import 'dart:ui';
import 'package:get/get.dart';
/// 应用程序的翻译类
class AppTranslations {
/// 启动页翻译类
static final KRSplashTranslations kr_splash = KRSplashTranslations();
/// 网络状态翻译类
static final KRNetworkStatusTranslations kr_networkStatus =
KRNetworkStatusTranslations();
/// 网络权限翻译类
static final KRNetworkPermissionTranslations kr_networkPermission =
KRNetworkPermissionTranslations();
/// 登录模块的翻译类
static final AppTranslationsLogin kr_login = AppTranslationsLogin();
/// 主页模块的翻译类
static final AppTranslationsHome kr_home = AppTranslationsHome();
/// 用户信息模块的翻译类
static final AppTranslationsUserInfo kr_userInfo = AppTranslationsUserInfo();
/// 设置模块的翻译类
static final AppTranslationsSetting kr_setting = AppTranslationsSetting();
/// 统计模块的翻译类
static final AppTranslationsStatistics kr_statistics =
AppTranslationsStatistics();
/// 邀请模块的翻译类
static final AppTranslationsInvite kr_invite = AppTranslationsInvite();
/// 消息模块的翻译类
static final AppTranslationsMessage kr_message = AppTranslationsMessage();
/// 套餐模块的翻译类
static final AppTranslationsPurchaseMembership kr_purchaseMembership =
AppTranslationsPurchaseMembership();
/// 订单状态模块的翻译类
static final AppTranslationsOrderStatus kr_orderStatus =
AppTranslationsOrderStatus();
/// 支付模块的翻译类
static final AppTranslationsPayment kr_payment = AppTranslationsPayment();
/// 对话框模块的翻译类
static final AppTranslationsDialog kr_dialog = AppTranslationsDialog();
/// 更新相关翻译
static final KRUpdateTranslations kr_update = KRUpdateTranslations();
/// 国家相关翻译
static final AppTranslationsCountry kr_country = AppTranslationsCountry();
/// 托盘相关翻译
static final AppTranslationsTray kr_tray = AppTranslationsTray();
/// 初始化翻译
static void kr_initTranslations() {
// Get.addTranslations({
// 'zh_CN': {
// 'login.welcome': '欢迎使用',
// 'login.verifyPhone': '验证手机号',
// // ... 其他翻译键值对
// },
// 'en_US': {
// 'login.welcome': 'Welcome',
// 'login.verifyPhone': 'Verify Phone',
// // ... 其他翻译键值对
// }
// });
// // 设置默认语言
// Get.locale = const Locale('zh', 'CN');
// // 设置备用语言
// Get.fallbackLocale = const Locale('en', 'US');
}
/// 切换语言
static void kr_changeLanguage(String languageCode, String countryCode) {
Get.updateLocale(Locale(languageCode, countryCode));
}
}
class AppTranslationsCountry {
String get cn => 'country.cn'.tr;
String get ir => 'country.ir'.tr;
String get af => 'country.af'.tr;
String get ru => 'country.ru'.tr;
String get tr => 'country.tr'.tr;
String get br => 'country.br'.tr;
String get id => 'country.id'.tr;
}
/// 登录模块的翻译类
class AppTranslationsLogin {
// Translations
/// 欢迎信息
String get welcome => 'login.welcome'.tr;
/// 验证手机号
String get verifyPhone => 'login.verifyPhone'.tr;
/// 验证邮箱
String get verifyEmail => 'login.verifyEmail'.tr;
/// 发送验证码信息,动态传递账号参数
/// [account] - 用户的账号信息
String codeSent(String account) =>
'login.codeSent'.trParams({'account': account});
/// 返回按钮文本
String get back => 'login.back'.tr;
/// 输入邮箱或手机号提示
String get enterEmailOrPhone => 'login.enterEmailOrPhone'.tr;
/// 输入验证码提示
String get enterCode => 'login.enterCode'.tr;
/// 输入密码提示
String get enterPassword => 'login.enterPassword'.tr;
/// 重新输入密码提示
String get reenterPassword => 'login.reenterPassword'.tr;
/// 忘记密码提示
String get forgotPassword => 'login.forgotPassword'.tr;
/// 验证码登录提示
String get codeLogin => 'login.codeLogin'.tr;
/// 密码登录提示
String get passwordLogin => 'login.passwordLogin'.tr;
/// 同意条款提示
String get agreeTerms => 'login.agreeTerms'.tr;
/// 服务条款
String get termsOfService => 'login.termsOfService'.tr;
/// 隐私政策
String get privacyPolicy => 'login.privacyPolicy'.tr;
/// 下一步按钮文本
String get next => 'login.next'.tr;
/// 立即注册按钮文本
String get registerNow => 'login.registerNow'.tr;
/// 设置并登录按钮文本
String get setAndLogin => 'login.setAndLogin'.tr;
/// 请输入账户提示
String get enterAccount => 'login.enterAccount'.tr;
/// 密码不匹配提示
String get passwordMismatch => 'login.passwordMismatch'.tr;
/// 发送验证码按钮文本
String get sendCode => 'login.sendCode'.tr;
/// 验证码已发送倒计时文本
String codeSentCountdown(int seconds) =>
'login.codeSentCountdown'.trParams({'seconds': seconds.toString()});
/// 和
String get and => 'login.and'.tr;
/// 邀请码输入提示
String get enterInviteCode => 'login.enterInviteCode'.tr;
/// 注册成功提示
String get registerSuccess => 'login.registerSuccess'.tr;
}
class AppTranslationsHome {
/// 欢迎信息
String get welcome => 'home.welcome'.tr;
/// 连接状态
String get disconnected => 'home.disconnected'.tr;
String get connecting => 'home.connecting'.tr;
String get connected => 'home.connected'.tr;
String get disconnecting => 'home.disconnecting'.tr;
/// 当前连接
String get currentConnectionTitle => 'home.currentConnectionTitle'.tr;
String get switchNode => 'home.switchNode'.tr;
String get timeout => 'home.timeout'.tr;
String get upload => 'home.upload'.tr;
String get download => 'home.download'.tr;
/// 加载状态
String get loading => 'home.loading'.tr;
String get error => 'home.error'.tr;
String get checkNetwork => 'home.checkNetwork'.tr;
String get retry => 'home.retry'.tr;
/// 连接区域
String get connectionSectionTitle => 'home.connectionSectionTitle'.tr;
String get dedicatedServers => 'home.dedicatedServers'.tr;
String get countryRegion => 'home.countryRegion'.tr;
/// 服务器列表
String get serverListTitle => 'home.serverListTitle'.tr;
String get noServers => 'home.noServers'.tr;
/// 节点列表
String get nodeListTitle => 'home.nodeListTitle'.tr;
String get noNodes => 'home.noNodes'.tr;
/// 国家/地区列表
String get countryListTitle => 'home.countryListTitle'.tr;
String get noRegions => 'home.noRegions'.tr;
/// 订阅卡片
String get subscriptionDescription => 'home.subscriptionDescription'.tr;
String get subscribe => 'home.subscribe'.tr;
/// 试用相关
String get trialPeriod => 'home.trialPeriod'.tr;
String get remainingTime => 'home.remainingTime'.tr;
String get trialExpired => 'home.trialExpired'.tr;
String get subscriptionExpired => 'home.subscriptionExpired'.tr;
/// 订阅更新提示
String get subscriptionUpdated => 'home.subscriptionUpdated'.tr;
String get subscriptionUpdatedMessage => 'home.subscriptionUpdatedMessage'.tr;
/// 试用状态
String get trialStatus => 'home.trialStatus'.tr;
/// 试用中
String get trialing => 'home.trialing'.tr;
/// 试用结束提示
String get trialEndMessage => 'home.trialEndMessage'.tr;
/// 最后一天订阅状态
String get lastDaySubscriptionStatus => 'home.lastDaySubscriptionStatus'.tr;
/// 最后一天订阅提示
String get lastDaySubscriptionMessage => 'home.lastDaySubscriptionMessage'.tr;
/// 订阅结束提示
String get subscriptionEndMessage => 'home.subscriptionEndMessage'.tr;
/// 试用时间格式化(带天数)
String trialTimeWithDays(int days, int hours, int minutes, int seconds) =>
'home.trialTimeWithDays'.trParams({
'days': days.toString(),
'hours': hours.toString(),
'minutes': minutes.toString(),
'seconds': seconds.toString(),
});
/// 试用时间格式化(带小时)
String trialTimeWithHours(int hours, int minutes, int seconds) =>
'home.trialTimeWithHours'.trParams({
'hours': hours.toString(),
'minutes': minutes.toString(),
'seconds': seconds.toString(),
});
/// 试用时间格式化(带分钟)
String trialTimeWithMinutes(int minutes, int seconds) =>
'home.trialTimeWithMinutes'.trParams({
'minutes': minutes.toString(),
'seconds': seconds.toString(),
});
/// 延迟测试相关
String get refreshLatency => 'home.refreshLatency'.tr;
String get testLatency => 'home.testLatency'.tr;
String get testing => 'home.testing'.tr;
String get refreshLatencyDesc => 'home.refreshLatencyDesc'.tr;
String get testAllNodesLatency => 'home.testAllNodesLatency'.tr;
/// 自动选择
String get autoSelect => 'home.autoSelect'.tr;
/// 已选择
String get selected => 'home.selected'.tr;
String get timeFormat => 'kr_time_format'.tr;
}
class AppTranslationsUserInfo {
// 用户信息页面相关翻译键
/// 页面标题
String get title => 'userInfo.title'.tr;
/// 绑定提示
String get bindingTip => 'userInfo.bindingTip'.tr;
/// 无有效订阅提示
String get noValidSubscription => 'userInfo.noValidSubscription'.tr;
/// 立即订阅按钮文本
String get subscribeNow => 'userInfo.subscribeNow'.tr;
/// 快捷键标题
String get shortcuts => 'userInfo.shortcuts'.tr;
/// 广告拦截开关文本
String get adBlock => 'userInfo.adBlock'.tr;
/// NDS解锁开关文本
String get ndsUnlock => 'userInfo.dnsUnlock'.tr;
/// 联系我们文本
String get contactUs => 'userInfo.contactUs'.tr;
/// 其他功能标题
String get others => 'userInfo.others'.tr;
/// 退出登录按钮文本
String get logout => 'userInfo.logout'.tr;
/// VPN官网入口文本
String get vpnWebsite => 'userInfo.vpnWebsite'.tr;
/// 推特入口文本
String get telegram => 'userInfo.telegram'.tr;
/// 邮箱入口文本
String get mail => 'userInfo.mail'.tr;
/// 电话入口文本
String get phone => 'userInfo.phone'.tr;
/// 人工客服支持入口文本
String get customerService => 'userInfo.customerService'.tr;
/// 联系客服人员入口文本
String get contactService => 'userInfo.contactService'.tr;
/// 我的账号
String get myAccount => 'userInfo.myAccount'.tr;
/// 请先登录账号
String get pleaseLogin => 'userInfo.pleaseLogin'.tr;
/// 订阅有效
String get subscriptionValid => 'userInfo.subscriptionValid'.tr;
/// 开始时间
String get startTime => 'userInfo.startTime'.tr;
/// 到期时间
String get expireTime => 'userInfo.expireTime'.tr;
/// 立即登录
String get loginNow => 'userInfo.loginNow'.tr;
/// 试用相关
String get trialPeriod => 'userInfo.trialPeriod'.tr;
String get remainingTime => 'userInfo.remainingTime'.tr;
String get trialExpired => 'userInfo.trialExpired'.tr;
String get subscriptionExpired => 'userInfo.subscriptionExpired'.tr;
/// 退出登录确认标题
String get logoutConfirmTitle => 'userInfo.logoutConfirmTitle'.tr;
/// 退出登录确认消息
String get logoutConfirmMessage => 'userInfo.logoutConfirmMessage'.tr;
/// 退出登录取消按钮文本
String get logoutCancel => 'userInfo.logoutCancel'.tr;
/// 复制成功提示
String get copySuccess => 'userInfo.copySuccess'.tr;
/// 暂无功能提示
String get notAvailable => 'userInfo.notAvailable'.tr;
/// 将被删除
String get willBeDeleted => 'userInfo.willBeDeleted'.tr;
/// 删除账号警告
String get deleteAccountWarning => 'userInfo.deleteAccountWarning'.tr;
/// 请求删除
String get requestDelete => 'userInfo.requestDelete'.tr;
String get switchSubscription => 'userInfo.switchSubscription'.tr;
String get trafficUsage => 'userInfo.trafficUsage'.tr;
String get deviceInfo => 'userInfo.deviceInfo'.tr;
String get trafficProgressTitle => 'userInfo.trafficProgress.title'.tr;
String get trafficProgressUnlimited => 'userInfo.trafficProgress.unlimited'.tr;
String get trafficProgressLimited => 'userInfo.trafficProgress.limited'.tr;
String get resetTraffic => 'userInfo.resetTraffic'.tr;
String get resetTrafficSuccess => 'userInfo.resetTrafficSuccess'.tr;
String get resetTrafficFailed => 'userInfo.resetTrafficFailed'.tr;
/// 设备限制
String get deviceLimit => 'userInfo.deviceLimit'.tr;
/// 余额
String get balance => 'userInfo.balance'.tr;
/// 重置
String get reset => 'userInfo.reset'.tr;
/// 流量重置标题
String get resetTrafficTitle => 'userInfo.resetTrafficTitle'.tr;
/// 流量重置消息
/// [currentTime] - 当前到期时间
/// [newTime] - 新的到期时间
String resetTrafficMessage(String currentTime, String newTime) =>
'userInfo.resetTrafficMessage'.trParams({
'currentTime': currentTime,
'newTime': newTime,
});
final String download = '下载';
final String upload = '上传';
}
class AppTranslationsSetting {
/// 设置页面标题
String get title => 'setting.title'.tr;
/// VPN连接
String get vpnConnection => 'setting.vpnConnection'.tr;
/// 通用
String get general => 'setting.general'.tr;
/// 模式
String get mode => 'setting.mode'.tr;
/// 自动连接
String get autoConnect => 'setting.autoConnect'.tr;
/// 路由规则
String get routeRule => 'setting.routeRule'.tr;
/// 选择国家
String get countrySelector => 'setting.countrySelector'.tr;
/// 选择国家描述
String get connectionTypeRuleRemark => 'setting.connectionTypeRuleRemark'.tr;
/// 全局代理备注
String get connectionTypeGlobalRemark => 'setting.connectionTypeGlobalRemark'.tr;
/// 直连备注
String get connectionTypeDirectRemark => 'setting.connectionTypeDirectRemark'.tr;
/// 外观
String get appearance => 'setting.appearance'.tr;
/// 通知
String get notifications => 'setting.notifications'.tr;
/// 帮助我们改进
String get helpImprove => 'setting.helpImprove'.tr;
/// 帮助我们改进的副标题
String get helpImproveSubtitle => 'setting.helpImproveSubtitle'.tr;
/// 请求删除账号
String get requestDeleteAccount => 'setting.requestDeleteAccount'.tr;
/// 去删除
String get goToDelete => 'setting.goToDelete'.tr;
/// 在 App Store 上为我们评分
String get rateUs => 'setting.rateUs'.tr;
/// IOS评分
String get iosRating => 'setting.iosRating'.tr;
/// 切换语言
String get switchLanguage => 'setting.switchLanguage'.tr;
/// 系统
String get system => 'setting.system'.tr;
/// 亮色
String get light => 'setting.light'.tr;
/// 暗色
String get dark => 'setting.dark'.tr;
/// 智能模式
String get vpnModeSmart => 'setting.vpnModeSmart'.tr;
/// 全局
String get connectionTypeGlobal => 'setting.connectionTypeGlobal'.tr;
/// 规则
String get connectionTypeRule => 'setting.connectionTypeRule'.tr;
/// 直连
String get connectionTypeDirect => 'setting.connectionTypeDirect'.tr;
/// 安全模式
String get vpnModeSecure => 'setting.secureMode'.tr;
/// 版本
String get version => 'setting.version'.tr;
}
class AppTranslationsStatistics {
/// 统计页面标题
String get title => 'statistics.title'.tr;
/// VPN 状态
String get vpnStatus => 'statistics.vpnStatus'.tr;
/// IP 地址
String get ipAddress => 'statistics.ipAddress'.tr;
/// 连接时间
String get connectionTime => 'statistics.connectionTime'.tr;
/// 协议
String get protocol => 'statistics.protocol'.tr;
/// 每周保护时间
String get weeklyProtectionTime => 'statistics.weeklyProtectionTime'.tr;
/// 当前连续记录
String get currentStreak => 'statistics.currentStreak'.tr;
/// 最高记录
String get highestStreak => 'statistics.highestStreak'.tr;
/// 最长连接时间
String get longestConnection => 'statistics.longestConnection'.tr;
/// 天数
String days(int days) =>
'statistics.days'.trParams({'days': days.toString()});
/// 星期几
String get monday => 'statistics.daysOfWeek.monday'.tr;
String get tuesday => 'statistics.daysOfWeek.tuesday'.tr;
String get wednesday => 'statistics.daysOfWeek.wednesday'.tr;
String get thursday => 'statistics.daysOfWeek.thursday'.tr;
String get friday => 'statistics.daysOfWeek.friday'.tr;
String get saturday => 'statistics.daysOfWeek.saturday'.tr;
String get sunday => 'statistics.daysOfWeek.sunday'.tr;
}
class AppTranslationsInvite {
/// 邀请页面标题
String get title => 'invite.title'.tr;
/// 邀请进度
String get progress => 'invite.progress'.tr;
/// 邀请统计
String get inviteStats => 'invite.inviteStats'.tr;
/// 已注册
String get registers => 'invite.registers'.tr;
/// 总佣金
String get totalCommission => 'invite.totalCommission'.tr;
/// 奖励明细
String get rewardDetails => 'invite.rewardDetails'.tr;
/// 邀请步骤
String get steps => 'invite.steps'.tr;
/// 邀请好友
String get inviteFriend => 'invite.inviteFriend'.tr;
/// 好友接受邀请
String get acceptInvite => 'invite.acceptInvite'.tr;
/// 获得奖励
String get getReward => 'invite.getReward'.tr;
/// 通过链接分享
String get shareLink => 'invite.shareLink'.tr;
/// 通过二维码分享
String get shareQR => 'invite.shareQR'.tr;
/// 邀请规则
String get rules => 'invite.rules'.tr;
/// 规则1
String get rule1 => 'invite.rule1'.tr;
/// 规则2
String get rule2 => 'invite.rule2'.tr;
/// 待下载
String get pending => 'invite.pending'.tr;
/// 在路上
String get processing => 'invite.processing'.tr;
/// 已成功
String get success => 'invite.success'.tr;
/// 已失效
String get expired => 'invite.expired'.tr;
/// 我的邀请码
String get myInviteCode => 'invite.myInviteCode'.tr;
/// 邀请码已复制到剪贴板
String get inviteCodeCopied => 'invite.inviteCodeCopied'.tr;
/// 已复制到剪贴板
String get copiedToClipboard => 'invite.copiedToClipboard'.tr;
/// 获取邀请码失败,请稍后重试
String get getInviteCodeFailed => 'invite.getInviteCodeFailed'.tr;
/// 生成二维码失败,请稍后重试
String get generateQRCodeFailed => 'invite.generateQRCodeFailed'.tr;
/// 生成分享链接失败,请稍后重试
String get generateShareLinkFailed => 'invite.generateShareLinkFailed'.tr;
/// 关闭
String get close => 'invite.close'.tr;
}
class AppTranslationsMessage {
/// 消息页面标题
String get title => 'message.title'.tr;
/// 系统消息
String get system => 'message.system'.tr;
/// 促销消息
String get promotion => 'message.promotion'.tr;
}
class AppTranslationsPurchaseMembership {
/// 购买套餐
String get purchasePackage => 'purchaseMembership.purchasePackage'.tr;
/// 暂无可用套餐
String get noData => 'purchaseMembership.noData'.tr;
/// 我的账号
String get myAccount => 'purchaseMembership.myAccount'.tr;
/// 选择套餐
String get selectPackage => 'purchaseMembership.selectPackage'.tr;
/// 套餐描述
String get packageDescription => 'purchaseMembership.packageDescription'.tr;
/// 支付方式
String get paymentMethod => 'purchaseMembership.paymentMethod'.tr;
/// 您可以随时在APP上取消
String get cancelAnytime => 'purchaseMembership.cancelAnytime'.tr;
/// 开始订阅
String get startSubscription => 'purchaseMembership.startSubscription'.tr;
/// 立即续订
String get renewNow => 'purchaseMembership.renewNow'.tr;
/// 流量限制
String get trafficLimit => 'purchaseMembership.trafficLimit'.tr;
/// 设备限制
String get deviceLimit => 'purchaseMembership.deviceLimit'.tr;
/// 套餐特性
String get features => 'purchaseMembership.features'.tr;
/// 展开
String get expand => 'purchaseMembership.expand'.tr;
/// 收起
String get collapse => 'purchaseMembership.collapse'.tr;
/// 订阅和隐私信息
String get subscriptionPrivacyInfo =>
'purchaseMembership.subscriptionPrivacyInfo'.tr;
/// 动态月份
String month(int months) =>
'purchaseMembership.month'.trParams({'months': months.toString()});
/// 动态年份
String year(int years) =>
'purchaseMembership.year'.trParams({'years': years.toString()});
/// 动态天数
String day(int days) =>
'purchaseMembership.day'.trParams({'days': days.toString()});
/// 套餐详情
String get planDetails => 'purchaseMembership.planDetails'.tr;
/// 套餐说明
String get planDescription => 'purchaseMembership.planDescription'.tr;
/// 查看详情
String get viewDetails => 'purchaseMembership.viewDetails'.tr;
/// 不限流量
String get unlimitedTraffic => 'purchaseMembership.unlimitedTraffic'.tr;
/// 不限设备
String get unlimitedDevices => 'purchaseMembership.unlimitedDevices'.tr;
/// 设备数量
String devices(String count) =>
'purchaseMembership.devices'.trParams({'count': count});
/// 确认购买
String get confirmPurchase => 'purchaseMembership.confirmPurchase'.tr;
/// 确认购买描述
String get confirmPurchaseDesc => 'purchaseMembership.confirmPurchaseDesc'.tr;
}
/// 订单状态模块的翻译类
class AppTranslationsOrderStatus {
/// 订单状态标题
String get title => 'orderStatus.title'.tr;
/// 待支付状态
String get pendingTitle => 'orderStatus.pending.title'.tr;
String get pendingDescription => 'orderStatus.pending.description'.tr;
/// 已支付状态
String get paidTitle => 'orderStatus.paid.title'.tr;
String get paidDescription => 'orderStatus.paid.description'.tr;
/// 支付成功状态
String get successTitle => 'orderStatus.success.title'.tr;
String get successDescription => 'orderStatus.success.description'.tr;
/// 订单关闭状态
String get closedTitle => 'orderStatus.closed.title'.tr;
String get closedDescription => 'orderStatus.closed.description'.tr;
/// 支付失败状态
String get failedTitle => 'orderStatus.failed.title'.tr;
String get failedDescription => 'orderStatus.failed.description'.tr;
/// 未知状态
String get unknownTitle => 'orderStatus.unknown.title'.tr;
String get unknownDescription => 'orderStatus.unknown.description'.tr;
/// 检查失败状态
String get checkFailedTitle => 'orderStatus.checkFailed.title'.tr;
String get checkFailedDescription => 'orderStatus.checkFailed.description'.tr;
/// 初始状态
String get initialTitle => 'orderStatus.initial.title'.tr;
String get initialDescription => 'orderStatus.initial.description'.tr;
}
/// 支付模块的翻译类
class AppTranslationsPayment {
/// 支付标题
String get title => 'payment.title'.tr;
/// 选择支付方式
String get selectMethod => 'payment.selectMethod'.tr;
/// 支付宝
String get alipay => 'payment.alipay'.tr;
/// 微信支付
String get wechat => 'payment.wechat'.tr;
/// 信用卡
String get creditCard => 'payment.creditCard'.tr;
/// PayPal
String get paypal => 'payment.paypal'.tr;
}
/// 翻译键常量
class KRTranslationKeys {
static const String kr_loginWelcome = 'login.welcome';
static const String kr_loginVerifyPhone = 'login.verifyPhone';
// ... 其他键定义
}
/// 对话框模块的翻译类
class AppTranslationsDialog {
/// 确认按钮文本
String get kr_confirm => 'dialog.confirm'.tr;
/// 取消按钮文本
String get kr_cancel => 'dialog.cancel'.tr;
/// 确定按钮文本
String get kr_ok => 'dialog.ok'.tr;
/// 我知道了按钮文本
String get kr_iKnow => 'dialog.iKnow'.tr;
}
/// 更新相关翻译
class KRUpdateTranslations {
/// 更新标题
String get title => 'update.title'.tr;
/// 更新内容
String get content => 'update.content'.tr;
/// 立即更新
String get updateNow => 'update.updateNow'.tr;
/// 稍后更新
String get updateLater => 'update.updateLater'.tr;
/// 默认更新内容
String get defaultContent => 'update.defaultContent'.tr;
}
/// 启动页翻译类
class KRSplashTranslations {
/// 应用名称
String get appName => 'splash.appName'.tr;
/// 欢迎标语
String get slogan => 'splash.slogan'.tr;
/// 初始化提示
String get initializing => 'splash.initializing'.tr;
/// 网络连接失败提示
String get kr_networkConnectionFailed => 'splash.networkConnectionFailure'.tr;
/// 重试按钮文本
String get kr_retry => 'splash.retry'.tr;
/// 网络权限失败提示
String get kr_networkPermissionFailed => 'splash.networkPermissionFailed'.tr;
/// 初始化失败提示
String get kr_initializationFailed => 'splash.initializationFailed'.tr;
}
/// 网络状态翻译类
class KRNetworkStatusTranslations {
/// 网络状态标题
String get title => 'network.status.title'.tr;
/// 检查网络连接
String get checkNetwork => 'network.status.checkNetwork'.tr;
/// 重试
String get retry => 'network.status.retry'.tr;
/// 取消
String get cancel => 'network.status.cancel'.tr;
/// 已连接
String get connected => 'network.status.connected'.tr;
/// 已断开
String get disconnected => 'network.status.disconnected'.tr;
/// 连接中
String get connecting => 'network.status.connecting'.tr;
/// 断开中
String get disconnecting => 'network.status.disconnecting'.tr;
/// 连接失败
String get connectionFailed => 'network.status.connectionFailed'.tr;
/// 断开失败
String get disconnectionFailed => 'network.status.disconnectionFailed'.tr;
/// 网络错误
String get networkError => 'network.status.networkError'.tr;
/// 网络超时
String get networkTimeout => 'network.status.networkTimeout'.tr;
/// 网络不可用
String get networkUnavailable => 'network.status.networkUnavailable'.tr;
/// 网络可用
String get networkAvailable => 'network.status.networkAvailable'.tr;
}
/// 网络权限翻译类
class KRNetworkPermissionTranslations {
/// 网络权限标题
String get title => 'network.permission.title'.tr;
/// 网络权限描述
String get description => 'network.permission.description'.tr;
/// 去设置
String get goToSettings => 'network.permission.goToSettings'.tr;
/// 取消
String get cancel => 'network.permission.cancel'.tr;
}
/// 托盘模块的翻译类
class AppTranslationsTray {
/// 打开仪表台
String get openDashboard => 'tray.open_dashboard'.tr;
/// 复制到终端
String get copyToTerminal => 'tray.copy_to_terminal'.tr;
/// 退出应用
String get exitApp => 'tray.exit_app'.tr;
}
+57
View File
@@ -0,0 +1,57 @@
// import 'package:get/get.dart';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
class GetxTranslations extends Translations {
final Map<String, Map<String, String>> _translations = {};
@override
Map<String, Map<String, String>> get keys => _translations;
// 初始化并加载所有翻译文件
Future<void> loadAllTranslations() async {
_translations['en'] = await _loadTranslations('assets/translations/strings_en.i18n.json');
_translations['zh_CN'] = await _loadTranslations('assets/translations/strings_zh.i18n.json');
_translations['zh_TW'] = await _loadTranslations('assets/translations/strings_zh_Hant.i18n.json');
_translations['es'] =
await _loadTranslations('assets/translations/strings_es.i18n.json');
_translations['ja'] =
await _loadTranslations('assets/translations/strings_ja.i18n.json');
_translations['ru'] =
await _loadTranslations('assets/translations/strings_ru.i18n.json');
_translations['et'] =
await _loadTranslations('assets/translations/strings_et.i18n.json');
}
// 读取并解析 JSON 文件
Future<Map<String, String>> _loadTranslations(String path) async {
final Map<String, String> translations = {};
final String jsonString = await rootBundle.loadString(path);
final Map<String, dynamic> jsonMap = json.decode(jsonString);
_flattenTranslations(jsonMap, translations);
return translations;
}
// 递归提取最底层的翻译文本并展平结构
void _flattenTranslations(
Map<String, dynamic> jsonMap, Map<String, String> translations,
[String prefix = '']) {
jsonMap.forEach((key, value) {
final newKey = prefix.isEmpty ? key : '$prefix.$key';
if (value is Map<String, dynamic>) {
_flattenTranslations(value, translations, newKey);
} else if (value is String) {
// 替换占位符 {xxx} 为 @xxx
final modifiedValue = value.replaceAllMapped(
RegExp(r'\{(\w+)\}'),
(match) => '@${match.group(1)}',
);
translations[newKey] = modifiedValue;
}
});
}
}
+146
View File
@@ -0,0 +1,146 @@
import 'dart:ui';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
enum KRLanguage {
en('🇬🇧', 'English', 'en'),
zh('🇨🇳', '中文', 'zh'),
es('🇪🇸', 'Español', 'es'),
zhHant('🇹🇼', '繁體中文', 'zhHant'),
ja('🇯🇵', '日本語', 'ja'),
ru('🇷🇺', 'Русский', 'ru'),
et('🇪🇪', 'Eesti', 'et');
final String flagEmoji;
final String languageName;
final String countryCode;
const KRLanguage(this.flagEmoji, this.languageName, this.countryCode);
}
class KRLanguageUtils {
static const String _lastLanguageKey = 'last_language';
static const String _initLanguageKey = 'init_language';
static final KRSecureStorage _storage = KRSecureStorage();
static final RxString kr_language = ''.obs;
// 获取可选语言列表
static List<KRLanguage> getAvailableLanguages() {
return KRLanguage.values;
}
// 切换语言
static Future<void> switchLanguage(KRLanguage language) async {
final locale = _getLocaleFromLanguage(language);
Get.updateLocale(locale);
await _saveLastLanguage(language);
kr_language.value = language.languageName;
}
// 获取当前语言
static KRLanguage getCurrentLanguage() {
final locale = Get.locale;
return _getLanguageFromLocale(locale);
}
// 获取最后保存的语言并转换为 Locale
static Future<Locale> getLastSavedLocale() async {
final lastLanguage = await _storage.kr_readData(key: _lastLanguageKey);
if (lastLanguage != null) {
final language = KRLanguage.values.firstWhere(
(lang) => lang.countryCode == lastLanguage,
orElse: () => KRLanguage.ru,
);
return _getLocaleFromLanguage(language);
}
return Locale('ru');
}
// 检查首次打开应用时的语言设置
static Future<bool> checkInitialLanguage() async {
final Locale? systemLocale = Get.deviceLocale;
final lastLanguage = await _storage.kr_readData(key: _initLanguageKey);
if (lastLanguage == null) {
final bool isChineseRegion = systemLocale?.languageCode == 'zh' &&
(systemLocale?.countryCode == 'CN' ||
systemLocale?.scriptCode == 'Hans');
_storage.kr_saveData(
key: _initLanguageKey, value: isChineseRegion.toString());
return isChineseRegion;
} else {
return false;
}
}
// 保存最后使用的语言
static Future<void> _saveLastLanguage(KRLanguage language) async {
await _storage.kr_saveData(
key: _lastLanguageKey, value: language.countryCode);
}
// 从语言枚举获取 Locale
static Locale _getLocaleFromLanguage(KRLanguage language) {
switch (language) {
case KRLanguage.zh:
return Locale('zh', 'CN');
case KRLanguage.es:
return Locale('es');
case KRLanguage.zhHant:
return Locale('zh', 'TW');
case KRLanguage.ja:
return Locale('ja');
case KRLanguage.ru:
return Locale('ru');
case KRLanguage.et:
return Locale('et');
default:
return Locale('en');
}
}
// 从 Locale 获取语言枚举
static KRLanguage _getLanguageFromLocale(Locale? locale) {
if (locale == null) return KRLanguage.en;
switch (locale.languageCode) {
case 'zh':
if (locale.countryCode == 'TW' || locale.scriptCode == 'Hant') {
return KRLanguage.zhHant;
}
return KRLanguage.zh;
case 'es':
return KRLanguage.es;
case 'ja':
return KRLanguage.ja;
case 'ru':
return KRLanguage.ru;
case 'et':
return KRLanguage.et;
default:
return KRLanguage.en;
}
}
// 获取当前语言编码字符串
static String getCurrentLanguageCode() {
final KRLanguage currentLanguage = getCurrentLanguage();
switch (currentLanguage) {
case KRLanguage.zh:
return 'zh_CN';
case KRLanguage.zhHant:
return 'zh_TW';
case KRLanguage.es:
return 'es';
case KRLanguage.ja:
return 'ja';
case KRLanguage.ru:
return 'ru';
case KRLanguage.et:
return 'et';
case KRLanguage.en:
return 'en';
}
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:get/get.dart';
/// 导航栏透明度管理 Mixin
/// 用于统一管理页面导航栏的透明度变化
mixin KRAppBarOpacityMixin {
/// 导航栏透明度值
final RxDouble kr_appBarOpacity = 0.0.obs;
/// 上次滚动位置
double _lastOffset = 0;
/// 上次更新时间
double _lastUpdateTime = 0;
/// 滚动阈值
static const double _scrollThreshold = 100.0;
/// 最小透明度
static const double _minOpacity = 0.0;
/// 最大透明度
static const double _maxOpacity = 1.0;
/// 滚动速度因子
static const double _scrollSpeedFactor = 0.5;
/// 更新导航栏透明度
/// [scrollPixels] 当前滚动位置
void kr_updateAppBarOpacity(double scrollPixels) {
final currentTime = DateTime.now().millisecondsSinceEpoch.toDouble();
final deltaTime = currentTime - _lastUpdateTime;
_lastUpdateTime = currentTime;
// 计算滚动速度
final scrollDelta = scrollPixels - _lastOffset;
final scrollSpeed = scrollDelta.abs() / (deltaTime > 0 ? deltaTime : 1);
_lastOffset = scrollPixels;
// 根据滚动位置计算基础透明度
double baseOpacity = 0.0;
if (scrollPixels <= 0) {
baseOpacity = _minOpacity;
} else if (scrollPixels >= _scrollThreshold) {
baseOpacity = _maxOpacity;
} else {
// 使用平滑的插值函数计算透明度
baseOpacity = (scrollPixels / _scrollThreshold).clamp(_minOpacity, _maxOpacity);
// 使用平方根函数使透明度变化更加平滑
baseOpacity = baseOpacity * baseOpacity;
}
// 根据滚动速度调整透明度
double speedFactor = (scrollSpeed * _scrollSpeedFactor).clamp(0.0, 0.5);
double targetOpacity = baseOpacity + (speedFactor * 0.2); // 减小速度影响
// 平滑过渡到目标透明度
final currentOpacity = kr_appBarOpacity.value;
final opacityDelta = targetOpacity - currentOpacity;
final smoothFactor = 0.3; // 平滑因子,值越小过渡越平滑
kr_appBarOpacity.value = (currentOpacity + opacityDelta * smoothFactor)
.clamp(_minOpacity, _maxOpacity);
}
}
+28
View File
@@ -0,0 +1,28 @@
import 'package:get/get.dart';
import 'kr_outbound_item.dart';
/// 表示服务器分组的模型类
class KRGroupOutboundList {
final String tag; // 标签
String icon = ""; // 图标
final List<KROutboundItem> outboundList; // 出站项列表
/// 构造函数,初始化标签和出站项列表
KRGroupOutboundList({
required this.tag,
required this.outboundList,
});
}
class KRCountryOutboundList {
final String country;
final List<KROutboundItem> outboundList;
//// 是否展开
RxBool isExpand = false.obs;
KRCountryOutboundList({
required this.country,
required this.outboundList,
});
}
+207
View File
@@ -0,0 +1,207 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_main/views/kr_main_view.dart';
import '../response/kr_node_list.dart';
/// 表示出站项的模型类
class KROutboundItem {
String id = ""; // 标签
String tag = ""; // 标签
String serverAddr = ""; // 服务器地址
/// 初始化配置
Map<String, dynamic> config = {}; // 配置项
String city = ""; // 城市
String country = ""; // 国家
double latitude = 0.0;
double longitude = 0.0;
String protocol = "";
/// 延迟
RxInt urlTestDelay = 0.obs;
/// URL
String url = "";
/// 服务器类型
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
KROutboundItem(KrNodeListItem nodeListItem) {
id = nodeListItem.id.toString();
protocol = nodeListItem.protocol;
latitude = nodeListItem.latitude;
longitude = nodeListItem.longitude;
tag = nodeListItem.name; // 设置标签
serverAddr = nodeListItem.serverAddr; // 设置服务器地址
// 将 config 字符串转换为 Map<String, dynamic>
city = nodeListItem.city; // 设置城市
country = nodeListItem.country; // 设置国家
final json = jsonDecode(nodeListItem.config);
switch (nodeListItem.protocol) {
case "vless":
final securityConfig =
json["security_config"] as Map<String, dynamic>? ?? {};
// 智能设置 server_name
String serverName = securityConfig["sni"] ?? "";
if (serverName.isEmpty) {
serverName = nodeListItem.serverAddr;
}
config = {
"type": "vless",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": json["port"],
"uuid": nodeListItem.uuid,
if (json["flow"] != null && json["flow"] != "none")
"flow": json["flow"],
if (json["transport"] != null && json["transport"] != "tcp")
"transport": _buildTransport(json),
"tls": {
"enabled": json["security"] == "tls",
"server_name": serverName,
"insecure": securityConfig["allow_insecure"] ?? false,
"utls": {
"enabled": true,
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
}
}
};
break;
case "vmess":
final securityConfig =
json["security_config"] as Map<String, dynamic>? ?? {};
// 智能设置 server_name
String serverName = securityConfig["sni"] ?? "";
if (serverName.isEmpty) {
serverName = nodeListItem.serverAddr;
}
config = {
"type": "vmess",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": json["port"],
"uuid": nodeListItem.uuid,
"alter_id": 0,
"security": "auto",
if (json["transport"] != null && json["transport"] != "tcp")
"transport": _buildTransport(json),
"tls": {
"enabled": json["security"] == "tls",
"server_name": serverName,
"insecure": securityConfig["allow_insecure"] ?? false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
break;
case "shadowsocks":
config = {
"type": "shadowsocks",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": json["port"],
"method": json["method"],
"password": nodeListItem.uuid
};
break;
case "hysteria2":
final securityConfig =
json["security_config"] as Map<String, dynamic>? ?? {};
config = {
"type": "hysteria2",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": json["port"],
"password": nodeListItem.uuid,
"up_mbps": 100,
"down_mbps": 100,
"obfs": {
"type": "salamander",
"password": json["obfs_password"] ?? nodeListItem.uuid
},
"tls": {
"enabled": true,
"server_name": securityConfig["sni"] ?? "",
"insecure": securityConfig["allow_insecure"] ?? false,
"alpn": ["h3"]
}
};
break;
case "trojan":
final securityConfig =
json["security_config"] as Map<String, dynamic>? ?? {};
// 智能设置 server_name
String serverName = securityConfig["sni"] ?? "";
if (serverName.isEmpty) {
// 如果没有配置 SNI,使用服务器地址
serverName = nodeListItem.serverAddr;
}
config = {
"type": "trojan",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": json["port"],
"password": nodeListItem.uuid,
"tls": {
"enabled": json["security"] == "tls",
"server_name": serverName,
"insecure": securityConfig["allow_insecure"] ?? false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
break;
}
// 检查 relayNode 是否为 JSON 字符串并解析
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
// 随机选择一个元素
final randomNode = (relayNodeJson..shuffle()).first;
config["server"] = randomNode["host"]; // 提取 host
config["server_port"] = randomNode["port"]; // 提取 port
}
}
// 解析配置
}
/// 构建传输配置
Map<String, dynamic> _buildTransport(Map<String, dynamic> json) {
final transportType = json["transport"] as String?;
final transportConfig =
json["transport_config"] as Map<String, dynamic>? ?? {};
switch (transportType) {
case "ws":
return {
"type": "ws",
"path": transportConfig["path"] ?? "/",
if (transportConfig["host"] != null)
"headers": {"Host": transportConfig["host"]}
};
case "grpc":
return {
"type": "grpc",
"service_name": transportConfig["service_name"] ?? ""
};
case "http":
return {
"type": "http",
"host": [transportConfig["host"] ?? ""],
"path": transportConfig["path"] ?? "/"
};
default:
return {};
}
}
}
+87
View File
@@ -0,0 +1,87 @@
import '../response/kr_node_group_list.dart';
import 'kr_group_outbound_list.dart';
import '../response/kr_node_list.dart';
import 'kr_outbound_item.dart';
/// 表示出站项列表的模型类
class KrOutboundsList {
/// 服务器分组
final List<KRGroupOutboundList> groupOutboundList = []; // 存储服务器分组的列表
/// 国家分组,包含所有国家
final List<KRCountryOutboundList> countryOutboundList = []; // 存储国家分组的列表
/// 全部列表
final List<KROutboundItem> allList = []; // 存储国家分组的列表
// 配置json
final List<Map<String,dynamic>> configJsonList = [];
/// 标签列表
final Map<String,KROutboundItem> keyList = {}; // 存储国家分组的列表
/// 处理出站项并将其分组
/// [list] 是要处理的出站项列表
void processOutboundItems(List<KrNodeListItem> list,List<KRNodeGroupListItem> groupList) {
final Map<String, List<KROutboundItem>> tagGroups = {};
final Map<String, List<KROutboundItem>> countryGroups = {};
// 用于追踪已使用的标签
final Map<String, int> tagCounter = {};
for (var element in list) {
// 生成唯一标签
var baseName = element.name;
if (tagCounter.containsKey(baseName)) {
tagCounter[baseName] = tagCounter[baseName]! + 1;
element.name = "${baseName}_${tagCounter[baseName]}";
} else {
tagCounter[baseName] = 0;
}
final KROutboundItem item = KROutboundItem(element);
allList.add(item);
// 根据标签分组出站项
for (var tag in element.tags) {
tagGroups.putIfAbsent(tag, () => []);
tagGroups[tag]?.add(item);
}
// 根据国家分组出站项
countryGroups.putIfAbsent(element.country, () => []);
countryGroups[element.country]?.add(item);
configJsonList.add(item.config);
keyList[item.tag] = item;
}
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
for (var tag in tagGroups.keys) {
final item = KRGroupOutboundList(
tag: tag, outboundList: tagGroups[tag]!);
for (var group in groupList) {
if (item.tag == group.name) {
item.icon = group.icon;
break;
}
}
groupOutboundList.add(item); // 添加标签分组到列表
}
// 将国家分组转换为 KRCountryOutboundList 并添加到 countryOutboundList
for (var country in countryGroups.keys) {
countryOutboundList.add(KRCountryOutboundList(
country: country,
outboundList: countryGroups[country]!)); // 添加国家分组到列表
}
}
}
View File
+64
View File
@@ -0,0 +1,64 @@
import 'package:kaer_with_panels/app/model/response/kr_is_register.dart';
import 'package:kaer_with_panels/app/model/response/kr_login_data.dart';
import 'package:kaer_with_panels/app/model/response/kr_node_list.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'response/kr_already_subscribe.dart';
import 'response/kr_config_data.dart';
import 'response/kr_kr_affiliate_count.dart';
import 'response/kr_message_list.dart';
import 'response/kr_node_group_list.dart';
import 'response/kr_order_status.dart';
import 'response/kr_payment_methods.dart';
import 'response/kr_purchase_order_no.dart';
import 'response/kr_status.dart';
import 'response/kr_user_available_subscribe.dart';
import 'response/kr_user_info.dart';
import 'response/kr_user_online_duration.dart';
import 'response/kr_web_text.dart';
/// json转换成实体类,每新建一个实体类就新增加一个case
abstract class EntityFromJsonUtil {
static T parseJsonToEntity<T>(Map<String, dynamic> json) {
switch (T.toString()) {
case "KRIsRegister":
return KRIsRegister.fromJson(json) as T;
case "KRLoginData":
return KRLoginData.fromJson(json) as T;
case "KRPackageList":
return KRPackageList.fromJson(json) as T;
case "KRNodeList":
return KRNodeList.fromJson(json) as T;
case "KRMessageList":
return KRMessageList.fromJson(json) as T;
case "KRUserInfo":
return KRUserInfo.fromJson(json) as T;
case "KRAffiliateCount":
return KRAffiliateCount.fromJson(json) as T;
case "KRPaymentMethods":
return KRPaymentMethods.fromJson(json) as T;
case "KRConfigData":
return KRConfigData.fromJson(json) as T;
case "KRPurchaseOrderNo":
return KRPurchaseOrderNo.fromJson(json) as T;
case "KRPurchaseOrderUrl":
return KRPurchaseOrderUrl.fromJson(json) as T;
case "KROrderStatus":
return KROrderStatus.fromJson(json) as T;
case "KRAlreadySubscribeList":
return KRAlreadySubscribeList.fromJson(json) as T;
case "KRNodeGroupList":
return KRNodeGroupList.fromJson(json) as T;
case "KRWebText":
return KRWebText.fromJson(json) as T;
case "KRUserOnlineDurationResponse":
return KRUserOnlineDurationResponse.fromJson(json) as T;
case "KRUserAvailableSubscribeList":
return KRUserAvailableSubscribeList.fromJson(json) as T;
case "KRStatus":
return KRStatus.fromJson(json) as T;
default:
throw ("类型转换错误,是否忘记添加了case!");
}
}
}
+7
View File
@@ -0,0 +1,7 @@
enum KRHomeViewsStatus {
kr_nore,
kr_serverList,
kr_subscribeList,
kr_coutrysubscribeList,
kr_serversubscribeList,
}
+4
View File
@@ -0,0 +1,4 @@
enum KRMessageType {
kr_payment,
kr_subscribe_update,
}
+18
View File
@@ -0,0 +1,18 @@
/// 登录类型
enum KRLoginType {
kr_telephone, /// 手机号
kr_email, /// 邮箱
}
extension KRLoginTypeExt on KRLoginType {
String get value {
switch (this) {
case KRLoginType.kr_email:
return "email";
case KRLoginType.kr_telephone:
return "mobile";
}
}
}
+73
View File
@@ -0,0 +1,73 @@
class KRAreaCodeItem {
final String kr_name; // 国家名称
final String kr_code; // 国家代码
final String kr_dialCode; // 国际拨号区号
final String kr_icon; // 图标(国旗)
KRAreaCodeItem({
required this.kr_name,
required this.kr_code,
required this.kr_dialCode,
required this.kr_icon,
});
// 从 Map 转换为模型对象
factory KRAreaCodeItem.fromMap(Map<String, dynamic> map) {
return KRAreaCodeItem(
kr_name: map['name'] ?? '',
kr_code: map['code'] ?? '',
kr_dialCode: map['dial_code'] ?? '',
kr_icon: map['icon'] ?? '',
);
}
// 将模型对象转换为 Map
Map<String, dynamic> toMap() {
return {
'name': kr_name,
'code': kr_code,
'dial_code': kr_dialCode,
'icon': kr_icon,
};
}
}
class KRAreaCode {
// 内部区域编码数据
static final List<Map<String, dynamic>> _kr_codeMap = [
{"name": "China", "code": "CN", "dial_code": "86", "icon": "🇨🇳"},
{"name": "United States", "code": "US", "dial_code": "1", "icon": "🇺🇸"},
{"name": "United Kingdom", "code": "GB", "dial_code": "44", "icon": "🇬🇧"},
{"name": "Canada", "code": "CA", "dial_code": "1", "icon": "🇨🇦"},
{"name": "Australia", "code": "AU", "dial_code": "61", "icon": "🇦🇺"},
{"name": "Germany", "code": "DE", "dial_code": "49", "icon": "🇩🇪"},
{"name": "France", "code": "FR", "dial_code": "33", "icon": "🇫🇷"},
{"name": "India", "code": "IN", "dial_code": "91", "icon": "🇮🇳"},
{"name": "Japan", "code": "JP", "dial_code": "81", "icon": "🇯🇵"},
{"name": "South Korea", "code": "KR", "dial_code": "82", "icon": "🇰🇷"},
{"name": "Russia", "code": "RU", "dial_code": "7", "icon": "🇷🇺"},
{"name": "Brazil", "code": "BR", "dial_code": "55", "icon": "🇧🇷"},
{"name": "South Africa", "code": "ZA", "dial_code": "27", "icon": "🇿🇦"},
{"name": "New Zealand", "code": "NZ", "dial_code": "64", "icon": "🇳🇿"},
{"name": "Singapore", "code": "SG", "dial_code": "65", "icon": "🇸🇬"},
{"name": "Hong Kong", "code": "HK", "dial_code": "852", "icon": "🇭🇰"},
{"name": "Taiwan", "code": "TW", "dial_code": "886", "icon": "🇹🇼"},
{"name": "Mexico", "code": "MX", "dial_code": "52", "icon": "🇲🇽"},
{"name": "Argentina", "code": "AR", "dial_code": "54", "icon": "🇦🇷"},
{"name": "Italy", "code": "IT", "dial_code": "39", "icon": "🇮🇹"},
{"name": "Spain", "code": "ES", "dial_code": "34", "icon": "🇪🇸"},
{"name": "Turkey", "code": "TR", "dial_code": "90", "icon": "🇹🇷"},
{"name": "Saudi Arabia", "code": "SA", "dial_code": "966", "icon": "🇸🇦"},
{
"name": "United Arab Emirates",
"code": "AE",
"dial_code": "971",
"icon": "🇦🇪"
}
];
// 获取区域编码的模型数组
static List<KRAreaCodeItem> kr_getCodeList() {
return _kr_codeMap.map((map) => KRAreaCodeItem.fromMap(map)).toList();
}
}
+33
View File
@@ -0,0 +1,33 @@
// ... existing code ...
class KRAlreadySubscribe {
final int subscribeId;
final int userSubscribeId;
const KRAlreadySubscribe({
required this.subscribeId,
required this.userSubscribeId,
});
factory KRAlreadySubscribe.fromJson(Map<String, dynamic> json) {
return KRAlreadySubscribe(
subscribeId: json['subscribe_id'] ?? 0,
userSubscribeId: json['user_subscribe_id'] ?? 0,
);
}
}
class KRAlreadySubscribeList {
final List<KRAlreadySubscribe> list;
KRAlreadySubscribeList({required this.list});
factory KRAlreadySubscribeList.fromJson(Map<String, dynamic> json) {
final List<dynamic> data = json['data'] ?? [];
return KRAlreadySubscribeList(
list: data.map((item) => KRAlreadySubscribe.fromJson(item)).toList(),
);
}
}
// ... existing code ...
+149
View File
@@ -0,0 +1,149 @@
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import '../../utils/kr_log_util.dart';
/// 配置数据模型
/// 用于存储应用程序的基础配置信息,包括加密信息、域名、启动图、官方联系方式等
class KRConfigData {
/// 配置信息
final String kr_config;
/// 加密密钥
final String kr_encryption_key;
/// 加密方法
final String kr_encryption_method;
/// 可用域名列表
final List<String> kr_domains;
/// 启动页图片URL
final String kr_startup_picture;
/// 启动页跳过等待时间(秒)
final int kr_startup_picture_skip_time;
/// 应用更新信息
final KRUpdateApplication kr_update_application;
/// 官方邮箱
final String kr_official_email;
/// 官方网站
final String kr_official_website;
/// 官方电报群
final String kr_official_telegram;
/// 官方电话
final String kr_official_telephone;
/// 邀请链接
final String kr_invitation_link;
final String kr_website_id;
KRConfigData({
this.kr_config = '',
this.kr_encryption_key = '',
this.kr_encryption_method = '',
List<String>? kr_domains,
this.kr_startup_picture = '',
this.kr_startup_picture_skip_time = 0,
KRUpdateApplication? kr_update_application,
this.kr_official_email = '',
this.kr_official_website = '',
this.kr_official_telegram = '',
this.kr_official_telephone = '',
this.kr_invitation_link = '',
this.kr_website_id = '',
}) : this.kr_domains = kr_domains ?? [],
this.kr_update_application =
kr_update_application ?? KRUpdateApplication();
factory KRConfigData.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_e('配置数据: $json', tag: 'KRConfigData');
return KRConfigData(
kr_invitation_link: json['invitation_link'] ?? '',
kr_config: json['kr_config'] ?? '',
kr_encryption_key: json['encryption_key'] ?? '',
kr_encryption_method: json['encryption_method'] ?? '',
kr_domains: List<String>.from(json['domains'] ?? []),
kr_startup_picture: json['startup_picture'] ?? '',
kr_startup_picture_skip_time: json['startup_picture_skip_time'] ?? 0,
kr_update_application:
KRUpdateApplication.fromJson(json['applications'] ?? {}),
kr_official_email: json['official_email'] ?? '',
kr_official_website: json['official_website'] ?? '',
kr_official_telegram: json['official_telegram'] ?? '',
kr_official_telephone: json['official_telephone'] ?? '',
kr_website_id: json['kr_website_id'] ?? '',
);
}
}
/// 应用更新信息模型
/// 用于存储应用程序的更新相关信息,包括版本号、下载地址等
class KRUpdateApplication {
/// 应用ID
final int kr_id;
/// 应用名称
final String kr_name;
/// 应用描述
final String kr_description;
/// 应用下载地址
final String kr_url;
/// 应用版本号
final String kr_version;
/// 版本更新说明
final String kr_version_description;
/// 是否为默认应用
final bool kr_is_default;
final String kr_version_review;
KRUpdateApplication({
this.kr_id = 0,
this.kr_name = '',
this.kr_description = '',
this.kr_url = '',
this.kr_version = '',
this.kr_version_description = '',
this.kr_is_default = false,
this.kr_version_review = '',
});
factory KRUpdateApplication.fromJson(Map<String, dynamic> json) {
return KRUpdateApplication(
kr_id: json['id'] ?? 0,
kr_name: json['name'] ?? '',
kr_description: json['description'] ?? '',
kr_url: json['url'] ?? '',
kr_version: json['version'] ?? '',
kr_version_description: json['version_description'] ?? '',
kr_is_default: json['is_default'] ?? false,
kr_version_review: json['version_review'] ?? '',
);
}
Future<bool> kr_is_daytime() async {
if (Platform.isIOS) {
if (kr_version_review.isNotEmpty) {
// 获取当前应用版本号
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
final String currentVersion = packageInfo.version;
// 比较版本号
return !(currentVersion == kr_version_review);
}
return true;
}
return true;
}
}
+22
View File
@@ -0,0 +1,22 @@
/// 是否注册
class KRIsRegister {
bool kr_isRegister = false;
KRIsRegister({this.kr_isRegister = false});
KRIsRegister.fromJson(Map<String, dynamic> json) {
kr_isRegister = json['Status'] == "true" || json['Status'] == true
? true
: false || json['status'] == "true" || json['status'] == true
? true
: false;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['Status'] = kr_isRegister;
return data;
}
}
+13
View File
@@ -0,0 +1,13 @@
class KRAffiliateCount {
int registers = -1;
int totalCommission = -1;
KRAffiliateCount({required this.registers, required this.totalCommission});
factory KRAffiliateCount.fromJson(Map<String, dynamic> json) {
return KRAffiliateCount(
registers: json['registers'] ?? -1,
totalCommission: json['total_commission'] ?? -1,
);
}
}
+17
View File
@@ -0,0 +1,17 @@
/// 登录信息
class KRLoginData {
String kr_token = "";
KRLoginData({this.kr_token = ""});
KRLoginData.fromJson(Map<String, dynamic> json) {
kr_token = json["token"].toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['token'] = kr_token;
return data;
}
}
+96
View File
@@ -0,0 +1,96 @@
class KRMessageList {
final int total;
final List<KRMessage> announcements;
KRMessageList({
this.total = 0,
List<KRMessage>? announcements,
}) : announcements = announcements ?? [];
factory KRMessageList.fromJson(Map<String, dynamic> json) {
return KRMessageList(
total: json['total'] as int? ?? 0,
announcements: (json['announcements'] as List<dynamic>?)
?.map((e) => KRMessage.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'total': total,
'announcements': announcements.map((e) => e.toJson()).toList(),
};
}
}
class KRMessage {
final int id;
final String title;
final String content;
final bool show;
final bool pinned;
final bool popup;
final int createdAt;
final int updatedAt;
final String dataStr = "";
// 通用时间格式化方法
String kr_formatDateTime(int timestamp, {String format = 'yyyy-MM-dd HH:mm'}) {
if (timestamp == 0) return '';
final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp );
return format
.replaceAll('yyyy', dateTime.year.toString())
.replaceAll('MM', dateTime.month.toString().padLeft(2, '0'))
.replaceAll('dd', dateTime.day.toString().padLeft(2, '0'))
.replaceAll('HH', dateTime.hour.toString().padLeft(2, '0'))
.replaceAll('mm', dateTime.minute.toString().padLeft(2, '0'))
.replaceAll('ss', dateTime.second.toString().padLeft(2, '0'));
}
// 获取格式化的创建时间字符串
String get kr_formattedCreatedAt => kr_formatDateTime(createdAt);
// 获取格式化的更新时间字符串
String get kr_formattedUpdatedAt => kr_formatDateTime(updatedAt);
KRMessage({
this.id = 0,
this.title = '',
this.content = '',
this.show = false,
this.pinned = false,
this.popup = false,
this.createdAt = 0,
this.updatedAt = 0,
});
factory KRMessage.fromJson(Map<String, dynamic> json) {
return KRMessage(
id: json['id'] as int? ?? 0,
title: json['title'] as String? ?? '',
content: json['content'] as String? ?? '',
show: json['show'] as bool? ?? false,
pinned: json['pinned'] as bool? ?? false,
popup: json['popup'] as bool? ?? false,
createdAt: json['created_at'] as int? ?? 0,
updatedAt: json['updated_at'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'show': show,
'pinned': pinned,
'popup': popup,
'created_at': createdAt,
'updated_at': updatedAt,
};
}
}
+40
View File
@@ -0,0 +1,40 @@
class KRNodeGroupList {
final List<KRNodeGroupListItem> list;
const KRNodeGroupList({required this.list});
factory KRNodeGroupList.fromJson(Map<String, dynamic> json) {
final dynamic listData = json['list'];
if (listData == null) return KRNodeGroupList(list: []);
try {
return KRNodeGroupList(
list: (listData as List)
.map((e) => KRNodeGroupListItem.fromJson(e))
.toList(),
);
} catch (e) {
return KRNodeGroupList(list: []);
}
}
}
class KRNodeGroupListItem {
final String id;
final String name;
final String icon;
const KRNodeGroupListItem({
required this.id,
required this.name,
required this.icon,
});
factory KRNodeGroupListItem.fromJson(Map<String, dynamic> json) {
return KRNodeGroupListItem(
id: json['id']?.toString() ?? '',
name: json['name']?.toString() ?? '',
icon: json['icon']?.toString() ?? '',
);
}
}
+153
View File
@@ -0,0 +1,153 @@
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRNodeList {
final List<KrNodeListItem> list;
final String subscribeId;
final String startTime;
final String expireTime;
const KRNodeList({
required this.list,
this.subscribeId = "0",
this.startTime = "",
this.expireTime = "",
});
factory KRNodeList.fromJson(Map<String, dynamic> json) {
try {
final List<dynamic>? jsonList= json['list'] as List<dynamic>?;
return KRNodeList(
list: jsonList?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: json['id']?.toString() ?? "0",
startTime: json['start_time']?.toString() ?? "",
expireTime: json['expire_time']?.toString() ?? "",
);
} catch (err) {
KRLogUtil.kr_e('KRNodeList解析错误: $err', tag: 'NodeList');
return const KRNodeList(list: []);
}
}
}
class KrNodeListItem {
final int id;
String name;
final String uuid;
final String protocol;
final String relayMode;
final String relayNode;
final String serverAddr;
final int speedLimit;
final List<String> tags;
final int traffic;
final double trafficRatio;
final int upload;
final String city;
final String config;
final String country;
final int createdAt;
final int download;
final String startTime;
final String expireTime;
final double latitude;
final double longitude;
KrNodeListItem({
required this.id,
required this.name,
required this.uuid,
required this.protocol,
this.relayMode = '',
this.relayNode = '',
required this.serverAddr,
required this.speedLimit,
required this.tags,
required this.traffic,
required this.trafficRatio,
required this.upload,
required this.city,
required this.config,
required this.country,
this.createdAt = 0,
required this.download,
required this.startTime,
required this.expireTime,
required this.latitude,
required this.longitude,
});
factory KrNodeListItem.fromJson(Map<String, dynamic> json) {
try {
return KrNodeListItem(
id: _parseIntSafely(json['id']),
name: json['name']?.toString() ?? '',
uuid: json['uuid']?.toString() ?? '',
protocol: json['protocol']?.toString() ?? '',
relayMode: json['relay_mode']?.toString() ?? '',
relayNode: json['relay_node']?.toString() ?? '',
serverAddr: json['server_addr']?.toString() ?? '',
speedLimit: _parseIntSafely(json['speed_limit']),
tags: _parseStringList(json['tags']),
traffic: _parseIntSafely(json['traffic']),
trafficRatio: _parseDoubleSafely(json['traffic_ratio']),
upload: _parseIntSafely(json['upload']),
city: json['city']?.toString() ?? '',
config: json['config']?.toString() ?? '',
country: json['country']?.toString() ?? '',
createdAt: _parseIntSafely(json['created_at']),
download: _parseIntSafely(json['download']),
startTime: json['start_time']?.toString() ?? '',
expireTime: json['expire_time']?.toString() ?? '',
latitude: _parseDoubleSafely(json['latitude']),
longitude: _parseDoubleSafely(json['longitude']),
);
} catch (err) {
KRLogUtil.kr_e('KrNodeListItem解析错误: $err', tag: 'NodeList');
return KrNodeListItem(
id: 0,
name: '',
uuid: '',
protocol: '',
serverAddr: '',
speedLimit: 0,
tags: [],
traffic: 0,
trafficRatio: 0,
upload: 0,
city: '',
config: '',
country: '',
download: 0,
startTime: '',
expireTime: '',
latitude: 0.0,
longitude: 0.0,
);
}
}
// 添加安全解析工具方法
static int _parseIntSafely(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
static double _parseDoubleSafely(dynamic value) {
if (value == null) return 0.0;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) return double.tryParse(value) ?? 0.0;
return 0.0;
}
static List<String> _parseStringList(dynamic value) {
if (value == null) return [];
if (value is List) {
return value.map((e) => e?.toString() ?? '').toList();
}
return [];
}
}
+323
View File
@@ -0,0 +1,323 @@
/// 订单状态模型类
class KROrderStatus {
/// 订单ID
final int kr_id;
/// 用户ID
final int kr_userId;
/// 订单编号
final String kr_orderNo;
/// 订单类型
final int kr_type;
/// 购买数量
final int kr_quantity;
/// 单价
final double kr_price;
/// 总金额
final double kr_amount;
/// 赠送金额
final double kr_giftAmount;
/// 折扣
final double kr_discount;
/// 优惠券码
final String? kr_coupon;
/// 优惠券折扣金额
final double kr_couponDiscount;
/// 佣金
final double kr_commission;
/// 支付方式
final String kr_method;
/// 手续费
final double kr_feeAmount;
/// 交易号
final String kr_tradeNo;
/// 订单状态
final int kr_status;
/// 订阅ID
final int kr_subscribeId;
/// 订阅信息
final KRSubscribe? kr_subscribe;
/// 创建时间
final int kr_createdAt;
/// 更新时间
final int kr_updatedAt;
/// 订单状态枚举
static const int kr_statusPending = 0; // 待支付
static const int kr_statusPaid = 1; // 已支付
static const int kr_statusCancelled = 2; // 已取消
static const int kr_statusRefunded = 3; // 已退款
static const int kr_statusFailed = 4; // 支付失败
/// 获取订单状态描述
String get kr_statusText {
switch (kr_status) {
case kr_statusPending:
return '待支付';
case kr_statusPaid:
return '已支付';
case kr_statusCancelled:
return '已取消';
case kr_statusRefunded:
return '已退款';
case kr_statusFailed:
return '支付失败';
default:
return '未知状态';
}
}
/// 判断订单是否待支付
bool get kr_isPending => kr_status == kr_statusPending;
/// 判断订单是否已支付
bool get kr_isPaid => kr_status == kr_statusPaid;
/// 判断订单是否已取消
bool get kr_isCancelled => kr_status == kr_statusCancelled;
/// 判断订单是否已退款
bool get kr_isRefunded => kr_status == kr_statusRefunded;
/// 判断订单是否支付失败
bool get kr_isFailed => kr_status == kr_statusFailed;
const KROrderStatus({
required this.kr_id,
required this.kr_userId,
required this.kr_orderNo,
required this.kr_type,
required this.kr_quantity,
required this.kr_price,
required this.kr_amount,
required this.kr_giftAmount,
required this.kr_discount,
this.kr_coupon,
required this.kr_couponDiscount,
required this.kr_commission,
required this.kr_method,
required this.kr_feeAmount,
required this.kr_tradeNo,
required this.kr_status,
required this.kr_subscribeId,
this.kr_subscribe,
required this.kr_createdAt,
required this.kr_updatedAt,
});
/// 从JSON映射创建订单状态实例
factory KROrderStatus.fromJson(Map<String, dynamic> json) {
return KROrderStatus(
kr_id: json['id'] as int? ?? 0,
kr_userId: json['user_id'] as int? ?? 0,
kr_orderNo: json['order_no'] as String? ?? '',
kr_type: json['type'] as int? ?? 0,
kr_quantity: json['quantity'] as int? ?? 0,
kr_price: (json['price'] as num?)?.toDouble() ?? 0.0,
kr_amount: (json['amount'] as num?)?.toDouble() ?? 0.0,
kr_giftAmount: (json['gift_amount'] as num?)?.toDouble() ?? 0.0,
kr_discount: (json['discount'] as num?)?.toDouble() ?? 0.0,
kr_coupon: json['coupon'] as String?,
kr_couponDiscount: (json['coupon_discount'] as num?)?.toDouble() ?? 0.0,
kr_commission: (json['commission'] as num?)?.toDouble() ?? 0.0,
kr_method: json['method'] as String? ?? '',
kr_feeAmount: (json['fee_amount'] as num?)?.toDouble() ?? 0.0,
kr_tradeNo: json['trade_no'] as String? ?? '',
kr_status: json['status'] as int? ?? 0,
kr_subscribeId: json['subscribe_id'] as int? ?? 0,
kr_subscribe: json['subscribe'] != null
? KRSubscribe.fromJson(json['subscribe'] as Map<String, dynamic>)
: null,
kr_createdAt: json['created_at'] as int? ?? 0,
kr_updatedAt: json['updated_at'] as int? ?? 0,
);
}
/// 转换为JSON映射
Map<String, dynamic> toJson() {
return {
'id': kr_id,
'user_id': kr_userId,
'order_no': kr_orderNo,
'type': kr_type,
'quantity': kr_quantity,
'price': kr_price,
'amount': kr_amount,
'gift_amount': kr_giftAmount,
'discount': kr_discount,
'coupon': kr_coupon,
'coupon_discount': kr_couponDiscount,
'commission': kr_commission,
'method': kr_method,
'fee_amount': kr_feeAmount,
'trade_no': kr_tradeNo,
'status': kr_status,
'subscribe_id': kr_subscribeId,
'subscribe': kr_subscribe?.toJson(),
'created_at': kr_createdAt,
'updated_at': kr_updatedAt,
};
}
}
/// 订阅信息模型类
class KRSubscribe {
final int kr_id;
final String kr_name;
final String kr_description;
final double kr_unitPrice;
final String kr_unitTime;
final List<KRDiscount> kr_discount;
final int kr_replacement;
final int kr_inventory;
final int kr_traffic;
final int kr_speedLimit;
final int kr_deviceLimit;
final int kr_quota;
final int kr_groupId;
final List<int> kr_serverGroup;
final List<int> kr_server;
final bool kr_show;
final bool kr_sell;
final int kr_sort;
final double kr_deductionRatio;
final bool kr_allowDeduction;
final int kr_resetCycle;
final bool kr_renewalReset;
final int kr_createdAt;
final int kr_updatedAt;
const KRSubscribe({
required this.kr_id,
required this.kr_name,
required this.kr_description,
required this.kr_unitPrice,
required this.kr_unitTime,
required this.kr_discount,
required this.kr_replacement,
required this.kr_inventory,
required this.kr_traffic,
required this.kr_speedLimit,
required this.kr_deviceLimit,
required this.kr_quota,
required this.kr_groupId,
required this.kr_serverGroup,
required this.kr_server,
required this.kr_show,
required this.kr_sell,
required this.kr_sort,
required this.kr_deductionRatio,
required this.kr_allowDeduction,
required this.kr_resetCycle,
required this.kr_renewalReset,
required this.kr_createdAt,
required this.kr_updatedAt,
});
factory KRSubscribe.fromJson(Map<String, dynamic> json) {
return KRSubscribe(
kr_id: json['id'] as int? ?? 0,
kr_name: json['name'] as String? ?? '',
kr_description: json['description'] as String? ?? '',
kr_unitPrice: (json['unit_price'] as num?)?.toDouble() ?? 0.0,
kr_unitTime: json['unit_time'] as String? ?? '',
kr_discount: (json['discount'] as List<dynamic>?)
?.map((e) => KRDiscount.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
kr_replacement: json['replacement'] as int? ?? 0,
kr_inventory: json['inventory'] as int? ?? 0,
kr_traffic: json['traffic'] as int? ?? 0,
kr_speedLimit: json['speed_limit'] as int? ?? 0,
kr_deviceLimit: json['device_limit'] as int? ?? 0,
kr_quota: json['quota'] as int? ?? 0,
kr_groupId: json['group_id'] as int? ?? 0,
kr_serverGroup: (json['server_group'] as List<dynamic>?)
?.map((e) => e as int)
.toList() ?? [],
kr_server: (json['server'] as List<dynamic>?)
?.map((e) => e as int)
.toList() ?? [],
kr_show: json['show'] as bool? ?? false,
kr_sell: json['sell'] as bool? ?? false,
kr_sort: json['sort'] as int? ?? 0,
kr_deductionRatio: (json['deduction_ratio'] as num?)?.toDouble() ?? 0.0,
kr_allowDeduction: json['allow_deduction'] as bool? ?? false,
kr_resetCycle: json['reset_cycle'] as int? ?? 0,
kr_renewalReset: json['renewal_reset'] as bool? ?? false,
kr_createdAt: json['created_at'] as int? ?? 0,
kr_updatedAt: json['updated_at'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': kr_id,
'name': kr_name,
'description': kr_description,
'unit_price': kr_unitPrice,
'unit_time': kr_unitTime,
'discount': kr_discount.map((e) => e.toJson()).toList(),
'replacement': kr_replacement,
'inventory': kr_inventory,
'traffic': kr_traffic,
'speed_limit': kr_speedLimit,
'device_limit': kr_deviceLimit,
'quota': kr_quota,
'group_id': kr_groupId,
'server_group': kr_serverGroup,
'server': kr_server,
'show': kr_show,
'sell': kr_sell,
'sort': kr_sort,
'deduction_ratio': kr_deductionRatio,
'allow_deduction': kr_allowDeduction,
'reset_cycle': kr_resetCycle,
'renewal_reset': kr_renewalReset,
'created_at': kr_createdAt,
'updated_at': kr_updatedAt,
};
}
}
/// 折扣信息模型类
class KRDiscount {
final int kr_quantity;
final double kr_discount;
const KRDiscount({
required this.kr_quantity,
required this.kr_discount,
});
factory KRDiscount.fromJson(Map<String, dynamic> json) {
return KRDiscount(
kr_quantity: json['quantity'] as int? ?? 0,
kr_discount: (json['discount'] as num?)?.toDouble() ?? 0.0,
);
}
Map<String, dynamic> toJson() {
return {
'quantity': kr_quantity,
'discount': kr_discount,
};
}
}
+327
View File
@@ -0,0 +1,327 @@
import 'dart:convert';
import 'package:get/get_connect/http/src/utils/utils.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../../utils/kr_common_util.dart';
class KRDescription {
final List<KRFeature> kr_features;
KRDescription({
required this.kr_features,
});
factory KRDescription.fromJson(Map<String, dynamic> json) {
return KRDescription(
kr_features: (json['features'] as List?)
?.map((e) => KRFeature.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
);
}
}
class KRFeature {
final String kr_label;
final String kr_type;
final List<KRFeatureDetail> kr_details;
KRFeature({
required this.kr_label,
required this.kr_type,
required this.kr_details,
});
factory KRFeature.fromJson(Map<String, dynamic> json) {
return KRFeature(
kr_label: json['label'] as String,
kr_type: json['type'] as String,
kr_details: (json['details'] as List)
.map((e) => KRFeatureDetail.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}
class KRFeatureDetail {
final String kr_label;
final String kr_description;
KRFeatureDetail({
required this.kr_label,
required this.kr_description,
});
factory KRFeatureDetail.fromJson(Map<String, dynamic> json) {
return KRFeatureDetail(
kr_label: json['label'] as String,
kr_description: json['description'] as String,
);
}
}
class KRPackageList {
final List<KRPackageListItem> kr_list;
final int kr_total;
KRPackageList({required this.kr_list, required this.kr_total});
// 获取所有不同的时间单位
List<String> kr_getUniqueUnitTimes() {
return kr_list.map((item) => item.kr_unitTime).toSet().toList();
}
// 根据时间单位获取套餐列表
List<KRPackageListItem> kr_getPackagesByUnitTime(String unitTime) {
return kr_list.where((item) => item.kr_unitTime == unitTime).toList();
}
// 检查是否有多个时间单位
bool kr_hasMultipleUnitTimes() {
return kr_getUniqueUnitTimes().length > 1;
}
factory KRPackageList.fromJson(Map<String, dynamic> json) {
return KRPackageList(
kr_list: (json['list'] as List? ?? [])
.map((item) => KRPackageListItem.fromJson(item))
.toList(),
kr_total: json['total']);
}
}
class KRPackageListItem {
// 包的唯一标识符
final int kr_id;
// 包的名称
final String kr_name;
// 包的描述信息
final KRDescription kr_description;
// 单位价格
final int kr_unitPrice;
// 单位时间(例如:月、年)
final String kr_unitTime;
// 折扣信息列表
final List<KRDiscount> kr_discount;
// 替换费用
final int kr_replacement;
// 库存数量
final int kr_inventory;
// 流量限制
final int kr_traffic;
// 速度限制
final int kr_speedLimit;
// 设备限制数量
final int kr_deviceLimit;
// 配额
final int kr_quota;
// 组ID
final int kr_groupId;
// 服务器组(可能为空)
final dynamic kr_serverGroup;
// 服务器(可能为空)
final dynamic kr_server;
// 是否显示
final bool kr_show;
// 是否出售
final bool kr_sell;
// 排序顺序
final int kr_sort;
// 扣除比例
final int kr_deductionRatio;
// 是否允许扣除
final bool kr_allowDeduction;
// 重置周期
final int kr_resetCycle;
// 是否在续订时重置
final bool kr_renewalReset;
// 创建时间戳
final int kr_createdAt;
// 更新时间戳
final int kr_updatedAt;
KRPackageListItem({
required this.kr_id,
required this.kr_name,
required this.kr_description,
required this.kr_unitPrice,
required this.kr_unitTime,
required this.kr_discount,
required this.kr_replacement,
required this.kr_inventory,
required this.kr_traffic,
required this.kr_speedLimit,
required this.kr_deviceLimit,
required this.kr_quota,
required this.kr_groupId,
this.kr_serverGroup,
this.kr_server,
required this.kr_show,
required this.kr_sell,
required this.kr_sort,
required this.kr_deductionRatio,
required this.kr_allowDeduction,
required this.kr_resetCycle,
required this.kr_renewalReset,
required this.kr_createdAt,
required this.kr_updatedAt,
});
// 从JSON数据创建KRPackageList实例
factory KRPackageListItem.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_i('json: ${json['traffic'] ?? 0}');
// 获取原始折扣列表
final List<KRDiscount> originalDiscounts = (json['discount'] as List<dynamic>?)
?.map((e) => KRDiscount.fromJson(e as Map<String, dynamic>))
.toList() ?? [];
// 创建基础选项(数量为1,折扣为100%)
final KRDiscount baseDiscount = KRDiscount(
kr_quantity: 1,
kr_discount: 100, // 折扣为100%,表示原价
);
// 创建完整的折扣列表,确保基础选项在最后
final List<KRDiscount> discounts = List.from(originalDiscounts);
if (!discounts.any((discount) => discount.kr_quantity == 1)) {
discounts.add(baseDiscount);
}
// 解析描述信息
final descriptionJson = json['description'];
KRDescription description;
if (descriptionJson is String) {
try {
description = KRDescription.fromJson(jsonDecode(descriptionJson));
} catch (e) {
KRLogUtil.kr_e('解析描述信息失败: $e');
description = KRDescription(kr_features: []);
}
} else if (descriptionJson is Map<String, dynamic>) {
description = KRDescription.fromJson(descriptionJson);
} else {
description = KRDescription(kr_features: []);
}
return KRPackageListItem(
kr_id: json['id'] as int,
kr_name: json['name'] as String,
kr_description: description,
kr_unitPrice: json['unit_price'] ?? 0,
kr_unitTime: json['unit_time'] as String,
kr_discount: discounts,
kr_replacement: json['replacement'] ?? 0,
kr_inventory: json['inventory'] ?? 0,
kr_traffic: json['traffic'] ?? 0,
kr_speedLimit: json['speed_limit'] ?? 0,
kr_deviceLimit: json['device_limit'] ?? 0,
kr_quota: json['quota'] ?? 0,
kr_groupId: json['group_id'] ?? 0,
kr_serverGroup: json['server_group'],
kr_server: json['server'],
kr_show: json['show'] ?? false,
kr_sell: json['sell'] ?? false,
kr_sort: json['sort'] ?? 0,
kr_deductionRatio: json['deduction_ratio'] ?? 0,
kr_allowDeduction: json['allow_deduction'] ?? false,
kr_resetCycle: json['reset_cycle'] ?? 0,
kr_renewalReset: json['renewal_reset'] ?? false,
kr_createdAt: json['created_at'] ?? 0,
kr_updatedAt: json['updated_at'] ?? 0,
);
}
// 获取包含基础选项的完整折扣列表
List<KRDiscount> kr_getCompleteDiscountList() {
// 创建基础选项(数量为1,折扣为100%)
final KRDiscount baseDiscount = KRDiscount(
kr_quantity: 1,
kr_discount: 100, // 折扣为100%,表示原价
);
// 如果原始折扣列表为空,返回只包含基础选项的列表
if (kr_discount.isEmpty) {
return [baseDiscount];
}
// 检查是否已存在数量为1的折扣
final bool hasBaseDiscount = kr_discount.any((discount) => discount.kr_quantity == 1);
// 创建新的列表,包含所有原始折扣
final List<KRDiscount> completeList = List.from(kr_discount);
// 如果没有数量为1的折扣,添加基础选项到列表末尾
if (!hasBaseDiscount) {
completeList.add(baseDiscount);
}
// 按数量排序
completeList.sort((a, b) => a.kr_quantity.compareTo(b.kr_quantity));
return completeList;
}
// 获取折扣后的价格
double kr_getDiscountedPrice() {
if (kr_discount.isEmpty) return kr_unitPrice / 100.0;
final maxDiscount = kr_discount.reduce((a, b) => a.kr_discount > b.kr_discount ? a : b);
return (kr_unitPrice / 100.0) * (maxDiscount.kr_discount / 100.0);
}
// 获取折扣显示文本
String kr_getDiscountDisplay() {
if (kr_discount.isEmpty) return '';
final maxDiscount = kr_discount.reduce((a, b) => a.kr_discount > b.kr_discount ? a : b);
return '${(maxDiscount.kr_discount / 10).toStringAsFixed(1)}';
}
// 获取最大折扣
KRDiscount? kr_getMaxDiscount() {
if (kr_discount.isEmpty) return null;
return kr_discount.reduce((a, b) =>
a.kr_discount > b.kr_discount ? a : b);
}
// 格式化价格显示(保留两位小数)
String kr_formatPrice(double price) {
return price.toStringAsFixed(2);
}
// 获取套餐描述
String kr_getPackageDescription() {
if (kr_discount.isEmpty) {
return '${kr_name} - ${kr_unitPrice / 100.0}元/${kr_unitTime}';
}
final maxDiscount = kr_discount.reduce((a, b) => a.kr_discount > b.kr_discount ? a : b);
return '${kr_name} - ${kr_getDiscountedPrice()}元/${kr_unitTime}';
}
}
class KRDiscount {
// 折扣数量
final int kr_quantity;
// 折扣百分比
final int kr_discount;
KRDiscount({
required this.kr_quantity,
required this.kr_discount,
});
// 从JSON数据创建KRDiscount实例
factory KRDiscount.fromJson(Map<String, dynamic> json) {
// 确保折扣值在 0-100 之间
int discount = json['discount'] ?? 100;
if (discount < 0) discount = 0;
if (discount > 100) discount = 100;
return KRDiscount(
kr_quantity: json['quantity'] ?? 1,
kr_discount: discount,
);
}
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRPaymentMethods {
/// 支付方式列表
final List<KRPaymentMethod> list;
KRPaymentMethods({required this.list});
factory KRPaymentMethods.fromJson(Map<String, dynamic> json) {
final List<dynamic> rawList = json['list'] ?? [];
return KRPaymentMethods(
list: rawList.map((item) => KRPaymentMethod.fromJson(item)).toList(),
);
}
}
class KRPaymentMethod {
final int id;
final String name;
final String platform;
final String icon;
final int feeMode;
final int feePercent;
final int feeAmount;
KRPaymentMethod({
required this.id,
required this.name,
required this.platform,
required this.icon,
required this.feeMode,
required this.feePercent,
required this.feeAmount,
});
factory KRPaymentMethod.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_i(json.toString());
return KRPaymentMethod(
id: (json['id'] ?? 0),
name: json['name'] ?? '',
platform: json['platform'] ?? '',
icon: json['icon'] ?? '',
feeMode: json['fee_mode'] ?? 0,
feePercent: json['fee_percent'] ?? 0,
feeAmount: json['fee_amount'] ?? 0,
);
}
}
+21
View File
@@ -0,0 +1,21 @@
class KRPurchaseOrderNo {
final String orderNo;
KRPurchaseOrderNo({required this.orderNo});
factory KRPurchaseOrderNo.fromJson(Map<String, dynamic> json) {
return KRPurchaseOrderNo(orderNo: json['order_no'] ?? '');
}
}
class KRPurchaseOrderUrl {
final String url;
KRPurchaseOrderUrl({required this.url});
factory KRPurchaseOrderUrl.fromJson(Map<String, dynamic> json) {
return KRPurchaseOrderUrl(url: json['checkout_url'] ?? '');
}
}
+22
View File
@@ -0,0 +1,22 @@
/// 是否注册
class KRStatus {
bool kr_bl= false;
KRStatus({this.kr_bl = false});
KRStatus.fromJson(Map<String, dynamic> json) {
kr_bl = json['Status'] == "true" || json['Status'] == true
? true
: false || json['status'] == "true" || json['status'] == true
? true
: false;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['status'] = kr_bl;
return data;
}
}
+71
View File
@@ -0,0 +1,71 @@
import '../../utils/kr_log_util.dart';
class KRUserAvailableSubscribeItem {
final int id;
final String name;
final int deviceLimit;
final int download;
final int upload;
final int traffic;
final String startTime;
final String expireTime;
final List<dynamic> list;
const KRUserAvailableSubscribeItem({
this.id = 0,
this.name = '',
this.deviceLimit = 0,
this.download = 0,
this.upload = 0,
this.traffic = 0,
this.startTime = '',
this.expireTime = '',
this.list = const [],
});
factory KRUserAvailableSubscribeItem.fromJson(Map<String, dynamic> json) {
return KRUserAvailableSubscribeItem(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? '',
deviceLimit: json['device_limit'] as int? ?? 0,
download: json['download'] as int? ?? 0,
upload: json['upload'] as int? ?? 0,
traffic: json['traffic'] as int? ?? 0,
startTime: json['start_time'] as String? ?? '',
expireTime: json['expire_time'] as String? ?? '',
list: (json['list'] as List<dynamic>?) ?? const [],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'device_limit': deviceLimit,
'download': download,
'upload': upload,
'traffic': traffic,
'start_time': startTime,
'expire_time': expireTime,
'list': list,
};
}
}
class KRUserAvailableSubscribeList {
final List<KRUserAvailableSubscribeItem> list;
const KRUserAvailableSubscribeList({
this.list = const [],
});
factory KRUserAvailableSubscribeList.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_i('订阅json列表: ${json}', tag: 'KRUserAvailableSubscribeList');
final List<dynamic> listData = (json['list'] as List<dynamic>?) ?? const [];
return KRUserAvailableSubscribeList(
list: listData
.map((item) => KRUserAvailableSubscribeItem.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
}
+34
View File
@@ -0,0 +1,34 @@
class KRUserInfo {
final int id;
final String email;
final int refererId;
final String referCode;
final String avatar;
final String areaCode;
final String telephone;
final int balance;
KRUserInfo({
required this.id,
required this.email,
this.refererId = 0,
this.referCode = '',
this.avatar = '',
this.areaCode = '',
this.telephone = '',
this.balance = 0
});
factory KRUserInfo.fromJson(Map<String, dynamic> json) {
return KRUserInfo(
id: json['id'] ?? 0,
email: json['email'] ?? '',
refererId: json['referer_id'] ?? 0,
referCode: json['refer_code'] ?? '',
avatar: json['avatar'] ?? '',
areaCode: json['area_code'] ?? '',
telephone: json['telephone'] ?? '',
balance: json['balance'] ?? 0,
);
}
}
+63
View File
@@ -0,0 +1,63 @@
/// 每日在线时长统计模型
class KRDailyOnlineStat {
final int day;
final String dayName;
final double hours;
KRDailyOnlineStat({
required this.day,
required this.dayName,
required this.hours,
});
factory KRDailyOnlineStat.fromJson(Map<String, dynamic> json) {
return KRDailyOnlineStat(
day: json['day'] ?? 0,
dayName: json['day_name'] ?? '',
hours: (json['hours'] ?? 0.0).toDouble(),
);
}
}
/// 在线时长记录模型
class KROnlineDurationRecord {
final int currentContinuousDays;
final int historyContinuousDays;
final int longestSingleConnection;
KROnlineDurationRecord({
required this.currentContinuousDays,
required this.historyContinuousDays,
required this.longestSingleConnection,
});
factory KROnlineDurationRecord.fromJson(Map<String, dynamic> json) {
return KROnlineDurationRecord(
currentContinuousDays: json['current_continuous_days'] ?? 0,
historyContinuousDays: json['history_continuous_days'] ?? 0,
longestSingleConnection: json['longest_single_connection'] ?? 0,
);
}
}
/// 用户在线时长统计响应模型
class KRUserOnlineDurationResponse {
final List<KRDailyOnlineStat> weeklyStats;
final KROnlineDurationRecord connectionRecords;
KRUserOnlineDurationResponse({
required this.weeklyStats,
required this.connectionRecords,
});
factory KRUserOnlineDurationResponse.fromJson(Map<String, dynamic> json) {
return KRUserOnlineDurationResponse(
weeklyStats: (json['weekly_stats'] as List?)
?.map((e) => KRDailyOnlineStat.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
connectionRecords: KROnlineDurationRecord.fromJson(
json['connection_records'] as Map<String, dynamic>? ?? {},
),
);
}
}
+30
View File
@@ -0,0 +1,30 @@
/// 网页文本内容响应模型
class KRWebText {
/// 隐私政策内容
final String privacyPolicy;
/// 用户协议内容
final String tosContent;
/// 构造函数
KRWebText({
required this.privacyPolicy,
required this.tosContent,
});
/// 从 JSON 创建实例
factory KRWebText.fromJson(Map<String, dynamic> json) {
return KRWebText(
privacyPolicy: json['privacy_policy'] ?? '',
tosContent: json['tos_content'] ?? '',
);
}
/// 转换为 JSON
Map<String, dynamic> toJson() {
return {
'privacy_policy': privacyPolicy,
'tos_content': tosContent,
};
}
}
@@ -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,
);
}
}
+80
View File
@@ -0,0 +1,80 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/mixins/kr_app_bar_opacity_mixin.dart';
import 'package:kaer_with_panels/app/model/entity_from_json_util.dart';
import '../utils/kr_aes_util.dart';
/// 接口返回基础类
class BaseResponse<T> {
late int retCode; //状态码
late String retMsg; //返回的信息
late Map<String, dynamic> body; // 返回的数据
late T model;
List<T> list = []; // 初始化为空列表
bool isSuccess = true; // 是否返回正确数据
BaseResponse.fromJson(Map<String, dynamic> json) {
retCode = json['code'];
final aes = AESUtils();
final dataMap = json['data'] ?? Map<String, dynamic>();
final cipherText = dataMap['data'] ?? "";
final nonce = dataMap['time'] ?? "";
if (cipherText.isNotEmpty && nonce.isNotEmpty) {
final encrypted = aes.decryptData(cipherText, "ne6t2qcz-szoa-rw78-egqz-lrsxxbl0dke3", nonce);
body = jsonDecode(encrypted);
}
else
{
body = dataMap;
}
if (retCode == 40004 || retCode == 40005 || retCode == 40002 || retCode == 40003) {
KRAppRunData().kr_loginOut();
}
if (retCode != 200) {
isSuccess = false;
}
retMsg = json['msg'];
// 获取错误信息
final msg = "error.${retCode.toString()}".tr;
if (msg.isNotEmpty && msg != "error.${retCode.toString()}") {
retMsg = msg;
}
if (body.isNotEmpty) {
if (body is List) {
list = (json['data'] as List<dynamic>)
.map((e) => EntityFromJsonUtil.parseJsonToEntity<T>(e))
.toList();
} else {
if (T == dynamic) {
model = body as T;
} else {
model = EntityFromJsonUtil.parseJsonToEntity<T>(body);
}
}
} else {
// 当body为空时,设置默认model值
}
}
// 获取泛型T的默认值
T _getDefaultValue<T>() {
if (T == String) return '' as T;
if (T == int) return 0 as T;
if (T == double) return 0.0 as T;
if (T == bool) return false as T;
if (T == Map) return {} as T;
if (T == List) return [] as T;
return null as T;
}
}
+10
View File
@@ -0,0 +1,10 @@
/// 接口返回的 -999等错误
class HttpError implements Exception {
int code;
String msg;
HttpError({required this.msg, required this.code});
@override
String toString() => 'ChatError(code: $code, msg: $msg)';
}
+281
View File
@@ -0,0 +1,281 @@
import 'dart:convert';
import 'dart:io' show Platform;
import 'package:dio/dio.dart';
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
import 'package:flutter_loggy_dio/flutter_loggy_dio.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/network/base_response.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
// import 'package:crypto/crypto.dart';
// import 'package:encrypt/encrypt.dart';
import 'package:loggy/loggy.dart';
import '../utils/kr_aes_util.dart';
import '../utils/kr_log_util.dart';
// import 'package:video/app/utils/common_util.dart';
// import 'package:video/app/utils/log_util.dart';
/// 定义请求方法的枚举
enum HttpMethod { GET, POST, DELETE }
/// 封装请求
class HttpUtil {
final Dio _dio = Dio();
static final HttpUtil _instance = HttpUtil._internal();
HttpUtil._internal() {
initDio();
}
factory HttpUtil() => _instance;
static HttpUtil getInstance() {
return _instance;
}
/// 对dio进行配置
void initDio() {
Loggy.initLoggy(logPrinter: PrettyPrinter());
_dio.interceptors.add(LoggyDioInterceptor(requestBody: true));
_dio.options.baseUrl = AppConfig.getInstance().baseUrl;
// 添加日志拦截器
_dio.interceptors.add(LoggyDioInterceptor(
requestBody: true,
responseBody: true,
));
// 设置连接超时时间
_dio.options.connectTimeout = const Duration(seconds: 60);
_dio.options.receiveTimeout = const Duration(seconds: 60);
_dio.options.sendTimeout = const Duration(seconds: 60);
// 设置请求头
_dio.options.headers = {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8',
// 移除固定的UserAgent,使用动态的
};
// 设置响应类型
_dio.options.responseType = ResponseType.json;
// 设置验证状态
_dio.options.validateStatus = (status) {
return status != null && status >= 200 && status < 500;
};
}
/// 更新baseUrl
void updateBaseUrl() {
String newBaseUrl = AppConfig.getInstance().baseUrl;
if (_dio.options.baseUrl != newBaseUrl) {
KRLogUtil.kr_i('🔄 更新baseUrl: ${_dio.options.baseUrl} -> $newBaseUrl', tag: 'HttpUtil');
_dio.options.baseUrl = newBaseUrl;
}
}
/// 初始化请求头 signature签名字符串
Map<String, dynamic> _initHeader(
String signature, String? userId, String? token) {
Map<String, dynamic> map = <String, dynamic>{};
if (KRAppRunData().kr_isLogin.value == true) {
map["Authorization"] = KRAppRunData().kr_token;
}
// 添加语言请求头
map["lang"] = KRLanguageUtils.getCurrentLanguageCode();
// 添加动态UserAgent头
map["User-Agent"] = _kr_getUserAgent();
return map;
}
/// 获取当前系统的 user_agent
String _kr_getUserAgent() {
if (Platform.isAndroid) {
return 'android';
} else if (Platform.isIOS) {
return 'ios';
} else if (Platform.isMacOS) {
return 'mac';
} else if (Platform.isWindows) {
return 'windows';
} else if (Platform.isLinux) {
return 'linux';
} else if (Platform.isFuchsia) {
return 'harmony';
} else {
return 'unknown';
}
}
/// request请求:T为转换的实体类, path:请求地址,query:请求参数, method: 请求方法, isShowLoading(可选): 是否显示加载中的状态,默认true显示, false为不显示
Future<BaseResponse<T>> request<T>(String path, Map<String, dynamic> params,
{HttpMethod method = HttpMethod.POST, bool isShowLoading = true}) async {
try {
// 每次请求前更新baseUrl,确保使用最新的域名
updateBaseUrl();
if (isShowLoading) {
KRCommonUtil.kr_showLoading();
}
var map = <String, dynamic>{};
if (path.contains("app")) {
final aes = AESUtils();
final plainText = jsonEncode(params);
map =
aes.encryptData(plainText, "ne6t2qcz-szoa-rw78-egqz-lrsxxbl0dke3");
} else {
map = params;
}
// 初始化请求头
final headers = _initHeader('signature', 'userId', 'token');
// 调试:打印请求头
KRLogUtil.kr_i('🔍 请求头: $headers', tag: 'HttpUtil');
Response<Map<String, dynamic>> responseTemp;
if (method == HttpMethod.GET) {
responseTemp = await _dio.get<Map<String, dynamic>>(
path,
queryParameters: map,
options: Options(
contentType: "application/json",
headers: headers, // 添加请求头
),
);
} else if (method == HttpMethod.DELETE) {
responseTemp = await _dio.delete<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, // 添加请求头
),
);
} else {
responseTemp = await _dio.post<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, // 添加请求头
),
);
}
if (isShowLoading) {
KRCommonUtil.kr_hideLoading();
}
return BaseResponse<T>.fromJson(responseTemp.data!);
} on DioException catch (err) {
if (isShowLoading) {
KRCommonUtil.kr_hideLoading();
}
int code = -90000;
String msg = "";
msg = err.message ?? err.type.toString();
switch (err.type) {
case DioExceptionType.connectionTimeout:
code = -90001;
break;
case DioExceptionType.sendTimeout:
code = -90002;
break;
case DioExceptionType.receiveTimeout:
code = -90003;
break;
case DioExceptionType.badResponse:
code = err.response?.statusCode ?? -90004;
break;
case DioExceptionType.cancel:
break;
case DioExceptionType.connectionError:
code = -90006;
break;
case DioExceptionType.badCertificate:
code = -90007;
break;
default:
if (err.error != null) {
if (err.error.toString().contains("Connection reset by peer")) {
code = -90008;
}
}
}
return BaseResponse<T>.fromJson({
'code': code,
'msg': msg,
'data': <String, dynamic>{}
});
} catch (e) {
if (isShowLoading) {
KRCommonUtil.kr_hideLoading();
}
return BaseResponse<T>.fromJson({
'code': -90000,
'msg': e.toString(),
'data': <String, dynamic>{}
});
}
}
}
/// 拦截器
class MyInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
KRLogUtil.kr_d(
'>>> Request │ ${options.method}${options.uri}\n'
'╔ Headers\n'
'${options.headers}\n'
'╚════════════════════════════════════════════════════════════════════════════════════════╝\n'
'╔ Body\n'
'${options.data}\n'
'╚════════════════════════════════════════════════════════════════════════════════════════╝',
tag: 'DioLoggy');
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
KRLogUtil.kr_d(
'<<< Response │ ${response.requestOptions.method}${response.statusCode} ${response.statusMessage}${response.requestOptions.uri}\n'
'╔ Body\n'
'${response.data}\n'
'╚════════════════════════════════════════════════════════════════════════════════════════╝',
tag: 'DioLoggy');
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
KRLogUtil.kr_e(
'<<< Error │ ${err.requestOptions.method}${err.requestOptions.uri}\n'
'╔ Error Type\n'
'${err.type}\n'
'╚════════════════════════════════════════════════════════════════════════════════════════╝\n'
'╔ Error Message\n'
'${err.message}\n'
'╚════════════════════════════════════════════════════════════════════════════════════════╝\n'
'╔ Response Data\n'
'${err.response?.data}\n'
'╚════════════════════════════════════════════════════════════════════════════════════════╝',
tag: 'DioLoggy');
handler.next(err);
}
}

Some files were not shown because too many files have changed in this diff Show More