初始化提交
This commit is contained in:
Executable
+1194
File diff suppressed because it is too large
Load Diff
Executable
+250
@@ -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();
|
||||
}
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+25
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+931
@@ -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;
|
||||
}
|
||||
Executable
+57
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+146
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+64
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
Executable
+207
@@ -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
@@ -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]!)); // 添加国家分组到列表
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Executable
Executable
+64
@@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
enum KRHomeViewsStatus {
|
||||
kr_nore,
|
||||
kr_serverList,
|
||||
kr_subscribeList,
|
||||
kr_coutrysubscribeList,
|
||||
kr_serversubscribeList,
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
enum KRMessageType {
|
||||
kr_payment,
|
||||
kr_subscribe_update,
|
||||
}
|
||||
Executable
+18
@@ -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";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+73
@@ -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
@@ -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 ...
|
||||
Executable
+149
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+96
@@ -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
@@ -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() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+153
@@ -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 [];
|
||||
}
|
||||
}
|
||||
Executable
+323
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Executable
+327
@@ -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
@@ -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
@@ -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'] ?? '');
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -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
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -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
@@ -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>? ?? {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+12
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+43
@@ -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
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+109
@@ -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
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1255
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+42
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1098
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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+181
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+506
@@ -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());
|
||||
}
|
||||
}
|
||||
+796
@@ -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
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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-7,1代表周一,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
@@ -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
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+10
@@ -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)';
|
||||
}
|
||||
Executable
+281
@@ -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
Reference in New Issue
Block a user