feat: 源码提交
Build Windows / build (push) Has been cancelled

This commit is contained in:
2025-10-19 23:30:54 -07:00
parent 54e7df78db
commit 75d4c48e41
6076 changed files with 64655 additions and 0 deletions
+1246
View File
File diff suppressed because it is too large Load Diff
+301
View File
@@ -0,0 +1,301 @@
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 '../services/kr_announcement_service.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;
/// 用户账号(使用响应式变量以便 UI 能监听变化)
final Rx<String?> kr_account = Rx<String?>(null);
/// 用户ID(使用响应式变量以便 UI 能监听变化)
final Rx<int?> kr_userId = Rx<int?>(null);
/// 登录类型
KRLoginType? kr_loginType;
/// 区号
String? kr_areaCode;
// 需要被监听的属性,用 obs 包装
final kr_isLogin = false.obs;
KRAppRunData._internal();
factory KRAppRunData() => _instance;
static KRAppRunData getInstance() {
return _instance;
}
/// 判断是否是设备登录(游客模式)
bool isDeviceLogin() {
// 设备登录的账号格式为 "device_设备ID"
return kr_account.value != null && kr_account.value!.startsWith('device_');
}
/// 从JWT token中解析userId
int? _kr_parseUserIdFromToken(String token) {
try {
// JWT格式: header.payload.signature
final parts = token.split('.');
if (parts.length != 3) {
KRLogUtil.kr_e('JWT token格式错误', tag: 'AppRunData');
return null;
}
// 解码payload部分(base64
String payload = parts[1];
// 手动添加必要的padding(base64要求长度是4的倍数)
switch (payload.length % 4) {
case 0:
break; // 不需要padding
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
default:
KRLogUtil.kr_e('JWT payload长度无效', tag: 'AppRunData');
return null;
}
final decodedBytes = base64.decode(payload);
final decodedString = utf8.decode(decodedBytes);
// 解析JSON
final Map<String, dynamic> payloadMap = jsonDecode(decodedString);
// 获取UserId
if (payloadMap.containsKey('UserId')) {
final userId = payloadMap['UserId'];
KRLogUtil.kr_i('从JWT解析出userId: $userId', tag: 'AppRunData');
return userId is int ? userId : int.tryParse(userId.toString());
}
return null;
} catch (e) {
KRLogUtil.kr_e('解析JWT token失败: $e', tag: 'AppRunData');
return null;
}
}
/// 保存用户信息
Future<void> kr_saveUserInfo(
String token,
String account,
KRLoginType loginType,
String? areaCode) async {
KRLogUtil.kr_i('开始保存用户信息', tag: 'AppRunData');
try {
// 更新内存中的数据
kr_token = token;
kr_account.value = account;
kr_loginType = loginType;
kr_areaCode = areaCode;
// 从JWT token中解析userId
kr_userId.value = _kr_parseUserIdFromToken(token);
KRLogUtil.kr_i('从JWT解析userId: ${kr_userId.value}', tag: 'AppRunData');
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 连接将在需要时建立
KRLogUtil.kr_i('用户信息已保存,跳过用户信息接口调用', 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.value = null;
kr_userId.value = null;
kr_loginType = null;
kr_areaCode = null;
// 删除存储的用户信息
await KRSecureStorage().kr_deleteData(key: _keyUserInfo);
// 重置公告显示状态
KRAnnouncementService().kr_reset();
// 重置主页面
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.value = userInfo['account'];
final loginTypeValue = userInfo['loginType'];
kr_loginType = KRLoginType.values.firstWhere(
(e) => e.value == loginTypeValue,
orElse: () => KRLoginType.kr_telephone,
);
kr_areaCode = userInfo['areaCode'] ?? "";
// 从token中解析userId
if (kr_token != null && kr_token!.isNotEmpty) {
kr_userId.value = _kr_parseUserIdFromToken(kr_token!);
}
KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=${kr_account.value}', tag: 'AppRunData');
// 验证token有效性
if (kr_token != null && kr_token!.isNotEmpty) {
KRLogUtil.kr_i('设置登录状态为true', tag: 'AppRunData');
kr_isLogin.value = true;
// 设备登录模式不需要调用用户信息接口
// 用户ID将从订阅信息或其他途径获取
KRLogUtil.kr_i('已登录,跳过用户信息接口调用', 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> _kr_connectSocket(String userId) async {
// 如果已存在连接,先断开
await _kr_disconnectSocket();
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'AppRunData');
KrSocketService.instance.kr_init(
baseUrl: AppConfig.getInstance().wsBaseUrl,
userId: userId,
deviceNumber: deviceId,
token: kr_token ?? "",
);
// 设置消息处理回调
KrSocketService.instance.setOnMessageCallback(_kr_handleMessage);
// 设置连接状态回调
KrSocketService.instance.setOnConnectionStateCallback(_kr_handleConnectionState);
// 建立连接
KrSocketService.instance.connect();
}
/// 处理接收到的消息
void _kr_handleMessage(Map<String, dynamic> message) {
try {
final String method = message['method'] as String;
switch (method) {
case 'kicked_device':
KRLogUtil.kr_i('超出登录设备限制', tag: 'AppRunData');
kr_loginOut();
break;
case 'kicked_admin':
KRLogUtil.kr_i('强制退出', tag: 'AppRunData');
kr_loginOut();
break;
case 'subscribe_update':
KRLogUtil.kr_i('订阅信息已更新', tag: 'AppRunData');
// 发送订阅更新事件
KREventBus().kr_sendMessage(KRMessageType.kr_subscribe_update);
break;
default:
KRLogUtil.kr_w('收到未知类型的消息: $message', tag: 'AppRunData');
}
} catch (e) {
KRLogUtil.kr_e('处理消息失败: $e', tag: 'AppRunData');
}
}
/// 处理连接状态变化
void _kr_handleConnectionState(bool isConnected) {
KRLogUtil.kr_i('WebSocket 连接状态: ${isConnected ? "已连接" : "已断开"}', tag: 'AppRunData');
}
/// 断开 Socket 连接
Future<void> _kr_disconnectSocket() async {
await KrSocketService.instance.disconnect();
}
}
+1
View File
@@ -0,0 +1 @@
+25
View File
@@ -0,0 +1,25 @@
class KRInviteProgress {
final int pending; // 待下载
final int processing; // 在路上
final int success; // 已成功
final int expired; // 已失效
final String? referCode; // 邀请码
KRInviteProgress({
this.pending = 0,
this.processing = 0,
this.success = 0,
this.expired = 0,
this.referCode,
});
factory KRInviteProgress.fromJson(Map<String, dynamic> json) {
return KRInviteProgress(
pending: json['pending'] ?? 0,
processing: json['processing'] ?? 0,
success: json['success'] ?? 0,
expired: json['expired'] ?? 0,
referCode: json['referCode'],
);
}
}
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
// import 'package:get/get.dart';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
class GetxTranslations extends Translations {
final Map<String, Map<String, String>> _translations = {};
@override
Map<String, Map<String, String>> get keys => _translations;
// 初始化并加载所有翻译文件
Future<void> loadAllTranslations() async {
_translations['en'] = await _loadTranslations('assets/translations/strings_en.i18n.json');
_translations['zh_CN'] = await _loadTranslations('assets/translations/strings_zh.i18n.json');
_translations['zh_TW'] = await _loadTranslations('assets/translations/strings_zh_Hant.i18n.json');
_translations['es'] =
await _loadTranslations('assets/translations/strings_es.i18n.json');
_translations['ja'] =
await _loadTranslations('assets/translations/strings_ja.i18n.json');
_translations['ru'] =
await _loadTranslations('assets/translations/strings_ru.i18n.json');
_translations['et'] =
await _loadTranslations('assets/translations/strings_et.i18n.json');
}
// 读取并解析 JSON 文件
Future<Map<String, String>> _loadTranslations(String path) async {
final Map<String, String> translations = {};
final String jsonString = await rootBundle.loadString(path);
final Map<String, dynamic> jsonMap = json.decode(jsonString);
_flattenTranslations(jsonMap, translations);
return translations;
}
// 递归提取最底层的翻译文本并展平结构
void _flattenTranslations(
Map<String, dynamic> jsonMap, Map<String, String> translations,
[String prefix = '']) {
jsonMap.forEach((key, value) {
final newKey = prefix.isEmpty ? key : '$prefix.$key';
if (value is Map<String, dynamic>) {
_flattenTranslations(value, translations, newKey);
} else if (value is String) {
// 替换占位符 {xxx} 为 @xxx
final modifiedValue = value.replaceAllMapped(
RegExp(r'\{(\w+)\}'),
(match) => '@${match.group(1)}',
);
translations[newKey] = modifiedValue;
}
});
}
}
+146
View File
@@ -0,0 +1,146 @@
import 'dart:ui';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
enum KRLanguage {
en('🇬🇧', 'English', 'en'),
zh('🇨🇳', '中文', 'zh'),
es('🇪🇸', 'Español', 'es'),
zhHant('🇹🇼', '繁體中文', 'zhHant'),
ja('🇯🇵', '日本語', 'ja'),
ru('🇷🇺', 'Русский', 'ru'),
et('🇪🇪', 'Eesti', 'et');
final String flagEmoji;
final String languageName;
final String countryCode;
const KRLanguage(this.flagEmoji, this.languageName, this.countryCode);
}
class KRLanguageUtils {
static const String _lastLanguageKey = 'last_language';
static const String _initLanguageKey = 'init_language';
static final KRSecureStorage _storage = KRSecureStorage();
static final RxString kr_language = ''.obs;
// 获取可选语言列表
static List<KRLanguage> getAvailableLanguages() {
return KRLanguage.values;
}
// 切换语言
static Future<void> switchLanguage(KRLanguage language) async {
final locale = _getLocaleFromLanguage(language);
Get.updateLocale(locale);
await _saveLastLanguage(language);
kr_language.value = language.languageName;
}
// 获取当前语言
static KRLanguage getCurrentLanguage() {
final locale = Get.locale;
return _getLanguageFromLocale(locale);
}
// 获取最后保存的语言并转换为 Locale
static Future<Locale> getLastSavedLocale() async {
final lastLanguage = await _storage.kr_readData(key: _lastLanguageKey);
if (lastLanguage != null) {
final language = KRLanguage.values.firstWhere(
(lang) => lang.countryCode == lastLanguage,
orElse: () => KRLanguage.ru,
);
return _getLocaleFromLanguage(language);
}
return Locale('ru');
}
// 检查首次打开应用时的语言设置
static Future<bool> checkInitialLanguage() async {
final Locale? systemLocale = Get.deviceLocale;
final lastLanguage = await _storage.kr_readData(key: _initLanguageKey);
if (lastLanguage == null) {
final bool isChineseRegion = systemLocale?.languageCode == 'zh' &&
(systemLocale?.countryCode == 'CN' ||
systemLocale?.scriptCode == 'Hans');
_storage.kr_saveData(
key: _initLanguageKey, value: isChineseRegion.toString());
return isChineseRegion;
} else {
return false;
}
}
// 保存最后使用的语言
static Future<void> _saveLastLanguage(KRLanguage language) async {
await _storage.kr_saveData(
key: _lastLanguageKey, value: language.countryCode);
}
// 从语言枚举获取 Locale
static Locale _getLocaleFromLanguage(KRLanguage language) {
switch (language) {
case KRLanguage.zh:
return Locale('zh', 'CN');
case KRLanguage.es:
return Locale('es');
case KRLanguage.zhHant:
return Locale('zh', 'TW');
case KRLanguage.ja:
return Locale('ja');
case KRLanguage.ru:
return Locale('ru');
case KRLanguage.et:
return Locale('et');
default:
return Locale('en');
}
}
// 从 Locale 获取语言枚举
static KRLanguage _getLanguageFromLocale(Locale? locale) {
if (locale == null) return KRLanguage.en;
switch (locale.languageCode) {
case 'zh':
if (locale.countryCode == 'TW' || locale.scriptCode == 'Hant') {
return KRLanguage.zhHant;
}
return KRLanguage.zh;
case 'es':
return KRLanguage.es;
case 'ja':
return KRLanguage.ja;
case 'ru':
return KRLanguage.ru;
case 'et':
return KRLanguage.et;
default:
return KRLanguage.en;
}
}
// 获取当前语言编码字符串
static String getCurrentLanguageCode() {
final KRLanguage currentLanguage = getCurrentLanguage();
switch (currentLanguage) {
case KRLanguage.zh:
return 'zh_CN';
case KRLanguage.zhHant:
return 'zh_TW';
case KRLanguage.es:
return 'es';
case KRLanguage.ja:
return 'ja';
case KRLanguage.ru:
return 'ru';
case KRLanguage.et:
return 'et';
case KRLanguage.en:
return 'en';
}
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:get/get.dart';
/// 导航栏透明度管理 Mixin
/// 用于统一管理页面导航栏的透明度变化
mixin KRAppBarOpacityMixin {
/// 导航栏透明度值
final RxDouble kr_appBarOpacity = 0.0.obs;
/// 上次滚动位置
double _lastOffset = 0;
/// 上次更新时间
double _lastUpdateTime = 0;
/// 滚动阈值
static const double _scrollThreshold = 100.0;
/// 最小透明度
static const double _minOpacity = 0.0;
/// 最大透明度
static const double _maxOpacity = 1.0;
/// 滚动速度因子
static const double _scrollSpeedFactor = 0.5;
/// 更新导航栏透明度
/// [scrollPixels] 当前滚动位置
void kr_updateAppBarOpacity(double scrollPixels) {
final currentTime = DateTime.now().millisecondsSinceEpoch.toDouble();
final deltaTime = currentTime - _lastUpdateTime;
_lastUpdateTime = currentTime;
// 计算滚动速度
final scrollDelta = scrollPixels - _lastOffset;
final scrollSpeed = scrollDelta.abs() / (deltaTime > 0 ? deltaTime : 1);
_lastOffset = scrollPixels;
// 根据滚动位置计算基础透明度
double baseOpacity = 0.0;
if (scrollPixels <= 0) {
baseOpacity = _minOpacity;
} else if (scrollPixels >= _scrollThreshold) {
baseOpacity = _maxOpacity;
} else {
// 使用平滑的插值函数计算透明度
baseOpacity = (scrollPixels / _scrollThreshold).clamp(_minOpacity, _maxOpacity);
// 使用平方根函数使透明度变化更加平滑
baseOpacity = baseOpacity * baseOpacity;
}
// 根据滚动速度调整透明度
double speedFactor = (scrollSpeed * _scrollSpeedFactor).clamp(0.0, 0.5);
double targetOpacity = baseOpacity + (speedFactor * 0.2); // 减小速度影响
// 平滑过渡到目标透明度
final currentOpacity = kr_appBarOpacity.value;
final opacityDelta = targetOpacity - currentOpacity;
final smoothFactor = 0.3; // 平滑因子,值越小过渡越平滑
kr_appBarOpacity.value = (currentOpacity + opacityDelta * smoothFactor)
.clamp(_minOpacity, _maxOpacity);
}
}
+28
View File
@@ -0,0 +1,28 @@
import 'package:get/get.dart';
import 'kr_outbound_item.dart';
/// 表示服务器分组的模型类
class KRGroupOutboundList {
final String tag; // 标签
String icon = ""; // 图标
final List<KROutboundItem> outboundList; // 出站项列表
/// 构造函数,初始化标签和出站项列表
KRGroupOutboundList({
required this.tag,
required this.outboundList,
});
}
class KRCountryOutboundList {
final String country;
final List<KROutboundItem> outboundList;
//// 是否展开
RxBool isExpand = false.obs;
KRCountryOutboundList({
required this.country,
required this.outboundList,
});
}
+319
View File
@@ -0,0 +1,319 @@
import 'dart:convert';
import 'package:get/get.dart';
import '../response/kr_node_list.dart';
/// 表示出站项的模型类
class KROutboundItem {
int selected = 0; // 是否选中(0=未选中,1=选中)
String id = ""; // 标签
String tag = ""; // 标签
String serverAddr = ""; // 服务器地址
/// 初始化配置
Map<String, dynamic> config = {}; // 配置项
String city = ""; // 城市
String country = ""; // 国家
double latitude = 0.0; // 节点纬度
double latitudeCountry = 0.0; // 国家中心纬度
double longitude = 0.0; // 节点经度
double longitudeCountry = 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;
latitudeCountry = nodeListItem.latitudeCountry;
longitude = nodeListItem.longitude;
longitudeCountry = nodeListItem.longitudeCountry;
tag = nodeListItem.name; // 设置标签
serverAddr = nodeListItem.serverAddr; // 设置服务器地址
// 将 config 字符串转换为 Map<String, dynamic>
city = nodeListItem.city; // 设置城市
country = nodeListItem.country; // 设置国家
// 安全解析 config 字段
// 新API格式:config为空,直接使用节点字段构建配置
// 旧API格式:config包含JSON配置
if (nodeListItem.config.isEmpty) {
print('️ 节点 ${nodeListItem.name} 使用直接字段构建配置');
_buildConfigFromFields(nodeListItem);
return;
}
late Map<String, dynamic> json;
try {
json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
} catch (e) {
print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段');
print('📄 Config 内容: ${nodeListItem.config}');
_buildConfigFromFields(nodeListItem);
return;
}
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 {};
}
}
/// 直接从节点字段构建配置(新API格式)
void _buildConfigFromFields(KrNodeListItem nodeListItem) {
switch (nodeListItem.protocol) {
case "shadowsocks":
config = {
"type": "shadowsocks",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"method": "chacha20-ietf-poly1305", // 默认加密方法
"password": nodeListItem.uuid
};
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
break;
case "vless":
config = {
"type": "vless",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"uuid": nodeListItem.uuid,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
}
};
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
break;
case "vmess":
config = {
"type": "vmess",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"uuid": nodeListItem.uuid,
"alter_id": 0,
"security": "auto",
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
break;
case "trojan":
config = {
"type": "trojan",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"password": nodeListItem.uuid,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}');
break;
case "hysteria2":
config = {
"type": "hysteria2",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"password": nodeListItem.uuid,
"up_mbps": 100,
"down_mbps": 100,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"alpn": ["h3"]
}
};
print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}');
break;
default:
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
config = {};
}
}
}
+94
View File
@@ -0,0 +1,94 @@
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);
// 检查节点配置是否有效(必须包含 type 字段)
if (item.config.isEmpty || !item.config.containsKey('type')) {
print('⚠️ 跳过无效节点: ${element.name},配置为空或缺少 type 字段');
continue; // 跳过无效节点
}
allList.add(item);
// 根据标签分组出站项
for (var tag in element.tags) {
tagGroups.putIfAbsent(tag, () => []);
tagGroups[tag]?.add(item);
}
// 根据国家分组出站项
countryGroups.putIfAbsent(element.country, () => []);
countryGroups[element.country]?.add(item);
configJsonList.add(item.config);
keyList[item.tag] = item;
}
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
for (var tag in tagGroups.keys) {
final item = KRGroupOutboundList(
tag: tag, outboundList: tagGroups[tag]!);
for (var group in groupList) {
if (item.tag == group.name) {
item.icon = group.icon;
break;
}
}
groupOutboundList.add(item); // 添加标签分组到列表
}
// 将国家分组转换为 KRCountryOutboundList 并添加到 countryOutboundList
for (var country in countryGroups.keys) {
countryOutboundList.add(KRCountryOutboundList(
country: country,
outboundList: countryGroups[country]!)); // 添加国家分组到列表
}
}
}
View File
+64
View File
@@ -0,0 +1,64 @@
import 'package:kaer_with_panels/app/model/response/kr_is_register.dart';
import 'package:kaer_with_panels/app/model/response/kr_login_data.dart';
import 'package:kaer_with_panels/app/model/response/kr_node_list.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'response/kr_already_subscribe.dart';
import 'response/kr_config_data.dart';
import 'response/kr_kr_affiliate_count.dart';
import 'response/kr_message_list.dart';
import 'response/kr_node_group_list.dart';
import 'response/kr_order_status.dart';
import 'response/kr_payment_methods.dart';
import 'response/kr_purchase_order_no.dart';
import 'response/kr_status.dart';
import 'response/kr_user_available_subscribe.dart';
import 'response/kr_user_info.dart';
import 'response/kr_user_online_duration.dart';
import 'response/kr_web_text.dart';
/// json转换成实体类,每新建一个实体类就新增加一个case
abstract class EntityFromJsonUtil {
static T parseJsonToEntity<T>(Map<String, dynamic> json) {
switch (T.toString()) {
case "KRIsRegister":
return KRIsRegister.fromJson(json) as T;
case "KRLoginData":
return KRLoginData.fromJson(json) as T;
case "KRPackageList":
return KRPackageList.fromJson(json) as T;
case "KRNodeList":
return KRNodeList.fromJson(json) as T;
case "KRMessageList":
return KRMessageList.fromJson(json) as T;
case "KRUserInfo":
return KRUserInfo.fromJson(json) as T;
case "KRAffiliateCount":
return KRAffiliateCount.fromJson(json) as T;
case "KRPaymentMethods":
return KRPaymentMethods.fromJson(json) as T;
case "KRConfigData":
return KRConfigData.fromJson(json) as T;
case "KRPurchaseOrderNo":
return KRPurchaseOrderNo.fromJson(json) as T;
case "KRPurchaseOrderUrl":
return KRPurchaseOrderUrl.fromJson(json) as T;
case "KROrderStatus":
return KROrderStatus.fromJson(json) as T;
case "KRAlreadySubscribeList":
return KRAlreadySubscribeList.fromJson(json) as T;
case "KRNodeGroupList":
return KRNodeGroupList.fromJson(json) as T;
case "KRWebText":
return KRWebText.fromJson(json) as T;
case "KRUserOnlineDurationResponse":
return KRUserOnlineDurationResponse.fromJson(json) as T;
case "KRUserAvailableSubscribeList":
return KRUserAvailableSubscribeList.fromJson(json) as T;
case "KRStatus":
return KRStatus.fromJson(json) as T;
default:
throw ("类型转换错误,是否忘记添加了case!");
}
}
}
+7
View File
@@ -0,0 +1,7 @@
enum KRHomeViewsStatus {
kr_nore,
kr_serverList,
kr_subscribeList,
kr_coutrysubscribeList,
kr_serversubscribeList,
}
+4
View File
@@ -0,0 +1,4 @@
enum KRMessageType {
kr_payment,
kr_subscribe_update,
}
+18
View File
@@ -0,0 +1,18 @@
/// 登录类型
enum KRLoginType {
kr_telephone, /// 手机号
kr_email, /// 邮箱
}
extension KRLoginTypeExt on KRLoginType {
String get value {
switch (this) {
case KRLoginType.kr_email:
return "email";
case KRLoginType.kr_telephone:
return "mobile";
}
}
}
+73
View File
@@ -0,0 +1,73 @@
class KRAreaCodeItem {
final String kr_name; // 国家名称
final String kr_code; // 国家代码
final String kr_dialCode; // 国际拨号区号
final String kr_icon; // 图标(国旗)
KRAreaCodeItem({
required this.kr_name,
required this.kr_code,
required this.kr_dialCode,
required this.kr_icon,
});
// 从 Map 转换为模型对象
factory KRAreaCodeItem.fromMap(Map<String, dynamic> map) {
return KRAreaCodeItem(
kr_name: map['name'] ?? '',
kr_code: map['code'] ?? '',
kr_dialCode: map['dial_code'] ?? '',
kr_icon: map['icon'] ?? '',
);
}
// 将模型对象转换为 Map
Map<String, dynamic> toMap() {
return {
'name': kr_name,
'code': kr_code,
'dial_code': kr_dialCode,
'icon': kr_icon,
};
}
}
class KRAreaCode {
// 内部区域编码数据
static final List<Map<String, dynamic>> _kr_codeMap = [
{"name": "China", "code": "CN", "dial_code": "86", "icon": "🇨🇳"},
{"name": "United States", "code": "US", "dial_code": "1", "icon": "🇺🇸"},
{"name": "United Kingdom", "code": "GB", "dial_code": "44", "icon": "🇬🇧"},
{"name": "Canada", "code": "CA", "dial_code": "1", "icon": "🇨🇦"},
{"name": "Australia", "code": "AU", "dial_code": "61", "icon": "🇦🇺"},
{"name": "Germany", "code": "DE", "dial_code": "49", "icon": "🇩🇪"},
{"name": "France", "code": "FR", "dial_code": "33", "icon": "🇫🇷"},
{"name": "India", "code": "IN", "dial_code": "91", "icon": "🇮🇳"},
{"name": "Japan", "code": "JP", "dial_code": "81", "icon": "🇯🇵"},
{"name": "South Korea", "code": "KR", "dial_code": "82", "icon": "🇰🇷"},
{"name": "Russia", "code": "RU", "dial_code": "7", "icon": "🇷🇺"},
{"name": "Brazil", "code": "BR", "dial_code": "55", "icon": "🇧🇷"},
{"name": "South Africa", "code": "ZA", "dial_code": "27", "icon": "🇿🇦"},
{"name": "New Zealand", "code": "NZ", "dial_code": "64", "icon": "🇳🇿"},
{"name": "Singapore", "code": "SG", "dial_code": "65", "icon": "🇸🇬"},
{"name": "Hong Kong", "code": "HK", "dial_code": "852", "icon": "🇭🇰"},
{"name": "Taiwan", "code": "TW", "dial_code": "886", "icon": "🇹🇼"},
{"name": "Mexico", "code": "MX", "dial_code": "52", "icon": "🇲🇽"},
{"name": "Argentina", "code": "AR", "dial_code": "54", "icon": "🇦🇷"},
{"name": "Italy", "code": "IT", "dial_code": "39", "icon": "🇮🇹"},
{"name": "Spain", "code": "ES", "dial_code": "34", "icon": "🇪🇸"},
{"name": "Turkey", "code": "TR", "dial_code": "90", "icon": "🇹🇷"},
{"name": "Saudi Arabia", "code": "SA", "dial_code": "966", "icon": "🇸🇦"},
{
"name": "United Arab Emirates",
"code": "AE",
"dial_code": "971",
"icon": "🇦🇪"
}
];
// 获取区域编码的模型数组
static List<KRAreaCodeItem> kr_getCodeList() {
return _kr_codeMap.map((map) => KRAreaCodeItem.fromMap(map)).toList();
}
}
+33
View File
@@ -0,0 +1,33 @@
// ... existing code ...
class KRAlreadySubscribe {
final int subscribeId;
final int userSubscribeId;
const KRAlreadySubscribe({
required this.subscribeId,
required this.userSubscribeId,
});
factory KRAlreadySubscribe.fromJson(Map<String, dynamic> json) {
return KRAlreadySubscribe(
subscribeId: json['subscribe_id'] ?? 0,
userSubscribeId: json['user_subscribe_id'] ?? 0,
);
}
}
class KRAlreadySubscribeList {
final List<KRAlreadySubscribe> list;
KRAlreadySubscribeList({required this.list});
factory KRAlreadySubscribeList.fromJson(Map<String, dynamic> json) {
final List<dynamic> data = json['data'] ?? [];
return KRAlreadySubscribeList(
list: data.map((item) => KRAlreadySubscribe.fromJson(item)).toList(),
);
}
}
// ... existing code ...
+149
View File
@@ -0,0 +1,149 @@
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import '../../utils/kr_log_util.dart';
/// 配置数据模型
/// 用于存储应用程序的基础配置信息,包括加密信息、域名、启动图、官方联系方式等
class KRConfigData {
/// 配置信息
final String kr_config;
/// 加密密钥
final String kr_encryption_key;
/// 加密方法
final String kr_encryption_method;
/// 可用域名列表
final List<String> kr_domains;
/// 启动页图片URL
final String kr_startup_picture;
/// 启动页跳过等待时间(秒)
final int kr_startup_picture_skip_time;
/// 应用更新信息
final KRUpdateApplication kr_update_application;
/// 官方邮箱
final String kr_official_email;
/// 官方网站
final String kr_official_website;
/// 官方电报群
final String kr_official_telegram;
/// 官方电话
final String kr_official_telephone;
/// 邀请链接
final String kr_invitation_link;
final String kr_website_id;
KRConfigData({
this.kr_config = '',
this.kr_encryption_key = '',
this.kr_encryption_method = '',
List<String>? kr_domains,
this.kr_startup_picture = '',
this.kr_startup_picture_skip_time = 0,
KRUpdateApplication? kr_update_application,
this.kr_official_email = '',
this.kr_official_website = '',
this.kr_official_telegram = '',
this.kr_official_telephone = '',
this.kr_invitation_link = '',
this.kr_website_id = '',
}) : this.kr_domains = kr_domains ?? [],
this.kr_update_application =
kr_update_application ?? KRUpdateApplication();
factory KRConfigData.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_e('配置数据: $json', tag: 'KRConfigData');
return KRConfigData(
kr_invitation_link: json['invitation_link'] ?? '',
kr_config: json['kr_config'] ?? '',
kr_encryption_key: json['encryption_key'] ?? '',
kr_encryption_method: json['encryption_method'] ?? '',
kr_domains: List<String>.from(json['domains'] ?? []),
kr_startup_picture: json['startup_picture'] ?? '',
kr_startup_picture_skip_time: json['startup_picture_skip_time'] ?? 0,
kr_update_application:
KRUpdateApplication.fromJson(json['applications'] ?? {}),
kr_official_email: json['official_email'] ?? '',
kr_official_website: json['official_website'] ?? '',
kr_official_telegram: json['official_telegram'] ?? '',
kr_official_telephone: json['official_telephone'] ?? '',
kr_website_id: json['kr_website_id'] ?? '',
);
}
}
/// 应用更新信息模型
/// 用于存储应用程序的更新相关信息,包括版本号、下载地址等
class KRUpdateApplication {
/// 应用ID
final int kr_id;
/// 应用名称
final String kr_name;
/// 应用描述
final String kr_description;
/// 应用下载地址
final String kr_url;
/// 应用版本号
final String kr_version;
/// 版本更新说明
final String kr_version_description;
/// 是否为默认应用
final bool kr_is_default;
final String kr_version_review;
KRUpdateApplication({
this.kr_id = 0,
this.kr_name = '',
this.kr_description = '',
this.kr_url = '',
this.kr_version = '',
this.kr_version_description = '',
this.kr_is_default = false,
this.kr_version_review = '',
});
factory KRUpdateApplication.fromJson(Map<String, dynamic> json) {
return KRUpdateApplication(
kr_id: json['id'] ?? 0,
kr_name: json['name'] ?? '',
kr_description: json['description'] ?? '',
kr_url: json['url'] ?? '',
kr_version: json['version'] ?? '',
kr_version_description: json['version_description'] ?? '',
kr_is_default: json['is_default'] ?? false,
kr_version_review: json['version_review'] ?? '',
);
}
Future<bool> kr_is_daytime() async {
if (Platform.isIOS) {
if (kr_version_review.isNotEmpty) {
// 获取当前应用版本号
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
final String currentVersion = packageInfo.version;
// 比较版本号
return !(currentVersion == kr_version_review);
}
return true;
}
return true;
}
}
+22
View File
@@ -0,0 +1,22 @@
/// 是否注册
class KRIsRegister {
bool kr_isRegister = false;
KRIsRegister({this.kr_isRegister = false});
KRIsRegister.fromJson(Map<String, dynamic> json) {
kr_isRegister = json['Status'] == "true" || json['Status'] == true
? true
: false || json['status'] == "true" || json['status'] == true
? true
: false;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['Status'] = kr_isRegister;
return data;
}
}
+13
View File
@@ -0,0 +1,13 @@
class KRAffiliateCount {
int registers = -1;
int totalCommission = -1;
KRAffiliateCount({required this.registers, required this.totalCommission});
factory KRAffiliateCount.fromJson(Map<String, dynamic> json) {
return KRAffiliateCount(
registers: json['registers'] ?? -1,
totalCommission: json['total_commission'] ?? -1,
);
}
}
+17
View File
@@ -0,0 +1,17 @@
/// 登录信息
class KRLoginData {
String kr_token = "";
KRLoginData({this.kr_token = ""});
KRLoginData.fromJson(Map<String, dynamic> json) {
kr_token = json["token"].toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['token'] = kr_token;
return data;
}
}
+96
View File
@@ -0,0 +1,96 @@
class KRMessageList {
final int total;
final List<KRMessage> announcements;
KRMessageList({
this.total = 0,
List<KRMessage>? announcements,
}) : announcements = announcements ?? [];
factory KRMessageList.fromJson(Map<String, dynamic> json) {
return KRMessageList(
total: json['total'] as int? ?? 0,
announcements: (json['announcements'] as List<dynamic>?)
?.map((e) => KRMessage.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'total': total,
'announcements': announcements.map((e) => e.toJson()).toList(),
};
}
}
class KRMessage {
final int id;
final String title;
final String content;
final bool show;
final bool pinned;
final bool popup;
final int createdAt;
final int updatedAt;
final String dataStr = "";
// 通用时间格式化方法
String kr_formatDateTime(int timestamp, {String format = 'yyyy-MM-dd HH:mm'}) {
if (timestamp == 0) return '';
final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp );
return format
.replaceAll('yyyy', dateTime.year.toString())
.replaceAll('MM', dateTime.month.toString().padLeft(2, '0'))
.replaceAll('dd', dateTime.day.toString().padLeft(2, '0'))
.replaceAll('HH', dateTime.hour.toString().padLeft(2, '0'))
.replaceAll('mm', dateTime.minute.toString().padLeft(2, '0'))
.replaceAll('ss', dateTime.second.toString().padLeft(2, '0'));
}
// 获取格式化的创建时间字符串
String get kr_formattedCreatedAt => kr_formatDateTime(createdAt);
// 获取格式化的更新时间字符串
String get kr_formattedUpdatedAt => kr_formatDateTime(updatedAt);
KRMessage({
this.id = 0,
this.title = '',
this.content = '',
this.show = false,
this.pinned = false,
this.popup = false,
this.createdAt = 0,
this.updatedAt = 0,
});
factory KRMessage.fromJson(Map<String, dynamic> json) {
return KRMessage(
id: json['id'] as int? ?? 0,
title: json['title'] as String? ?? '',
content: json['content'] as String? ?? '',
show: json['show'] as bool? ?? false,
pinned: json['pinned'] as bool? ?? false,
popup: json['popup'] as bool? ?? false,
createdAt: json['created_at'] as int? ?? 0,
updatedAt: json['updated_at'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'show': show,
'pinned': pinned,
'popup': popup,
'created_at': createdAt,
'updated_at': updatedAt,
};
}
}
+40
View File
@@ -0,0 +1,40 @@
class KRNodeGroupList {
final List<KRNodeGroupListItem> list;
const KRNodeGroupList({required this.list});
factory KRNodeGroupList.fromJson(Map<String, dynamic> json) {
final dynamic listData = json['list'];
if (listData == null) return KRNodeGroupList(list: []);
try {
return KRNodeGroupList(
list: (listData as List)
.map((e) => KRNodeGroupListItem.fromJson(e))
.toList(),
);
} catch (e) {
return KRNodeGroupList(list: []);
}
}
}
class KRNodeGroupListItem {
final String id;
final String name;
final String icon;
const KRNodeGroupListItem({
required this.id,
required this.name,
required this.icon,
});
factory KRNodeGroupListItem.fromJson(Map<String, dynamic> json) {
return KRNodeGroupListItem(
id: json['id']?.toString() ?? '',
name: json['name']?.toString() ?? '',
icon: json['icon']?.toString() ?? '',
);
}
}
+187
View File
@@ -0,0 +1,187 @@
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;
final bool isTryOut; // 是否是试用订阅
const KRNodeList({
required this.list,
this.subscribeId = "0",
this.startTime = "",
this.expireTime = "",
this.isTryOut = false,
});
factory KRNodeList.fromJson(Map<String, dynamic> json) {
try {
// 新的 API 返回格式: {"list": [{"id": 24, "is_try_out": true, "nodes": [...]}]}
final List<dynamic>? listData = json['list'] as List<dynamic>?;
if (listData == null || listData.isEmpty) {
KRLogUtil.kr_w('节点列表为空', tag: 'NodeList');
return const KRNodeList(list: []);
}
// 获取第一个订阅对象
final subscribeData = listData[0] as Map<String, dynamic>;
final bool isTryOut = subscribeData['is_try_out'] as bool? ?? false;
final List<dynamic>? nodesData = subscribeData['nodes'] as List<dynamic>?;
KRLogUtil.kr_i('节点列表解析: is_try_out=$isTryOut, 节点数=${nodesData?.length ?? 0}', tag: 'NodeList');
return KRNodeList(
list: nodesData?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: subscribeData['id']?.toString() ?? "0",
startTime: subscribeData['start_time']?.toString() ?? "",
expireTime: subscribeData['expire_time']?.toString() ?? "",
isTryOut: isTryOut,
);
} 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 port; // 新增:端口字段
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 latitudeCountry;
final double longitude;
final double longitudeCountry;
KrNodeListItem({
required this.id,
required this.name,
required this.uuid,
required this.protocol,
this.relayMode = '',
this.relayNode = '',
required this.serverAddr,
this.port = 0, // 默认值
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.latitudeCountry,
required this.longitude,
required this.longitudeCountry,
});
factory KrNodeListItem.fromJson(Map<String, dynamic> json) {
try {
// 支持新旧两种API格式
// 新格式: address, port
// 旧格式: server_addr, config 中包含 port
final serverAddr = json['address']?.toString() ?? json['server_addr']?.toString() ?? '';
final port = _parseIntSafely(json['port']);
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: serverAddr,
port: port,
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']),
latitudeCountry: _parseDoubleSafely(json['latitude_country']),
longitude: _parseDoubleSafely(json['longitude']),
longitudeCountry: _parseDoubleSafely(json['longitude_country']),
);
} catch (err) {
KRLogUtil.kr_e('KrNodeListItem解析错误: $err', tag: 'NodeList');
return KrNodeListItem(
id: 0,
name: '',
uuid: '',
protocol: '',
serverAddr: '',
port: 0,
speedLimit: 0,
tags: [],
traffic: 0,
trafficRatio: 0,
upload: 0,
city: '',
config: '',
country: '',
download: 0,
startTime: '',
expireTime: '',
latitude: 0.0,
latitudeCountry: 0.0,
longitude: 0.0,
longitudeCountry: 0.0,
);
}
}
// 添加安全解析工具方法
static int _parseIntSafely(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
static double _parseDoubleSafely(dynamic value) {
if (value == null) return 0.0;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) return double.tryParse(value) ?? 0.0;
return 0.0;
}
static List<String> _parseStringList(dynamic value) {
if (value == null) return [];
if (value is List) {
return value.map((e) => e?.toString() ?? '').toList();
}
return [];
}
}
+323
View File
@@ -0,0 +1,323 @@
/// 订单状态模型类
class KROrderStatus {
/// 订单ID
final int kr_id;
/// 用户ID
final int kr_userId;
/// 订单编号
final String kr_orderNo;
/// 订单类型
final int kr_type;
/// 购买数量
final int kr_quantity;
/// 单价
final double kr_price;
/// 总金额
final double kr_amount;
/// 赠送金额
final double kr_giftAmount;
/// 折扣
final double kr_discount;
/// 优惠券码
final String? kr_coupon;
/// 优惠券折扣金额
final double kr_couponDiscount;
/// 佣金
final double kr_commission;
/// 支付方式
final String kr_method;
/// 手续费
final double kr_feeAmount;
/// 交易号
final String kr_tradeNo;
/// 订单状态
final int kr_status;
/// 订阅ID
final int kr_subscribeId;
/// 订阅信息
final KRSubscribe? kr_subscribe;
/// 创建时间
final int kr_createdAt;
/// 更新时间
final int kr_updatedAt;
/// 订单状态枚举
static const int kr_statusPending = 0; // 待支付
static const int kr_statusPaid = 1; // 已支付
static const int kr_statusCancelled = 2; // 已取消
static const int kr_statusRefunded = 3; // 已退款
static const int kr_statusFailed = 4; // 支付失败
/// 获取订单状态描述
String get kr_statusText {
switch (kr_status) {
case kr_statusPending:
return '待支付';
case kr_statusPaid:
return '已支付';
case kr_statusCancelled:
return '已取消';
case kr_statusRefunded:
return '已退款';
case kr_statusFailed:
return '支付失败';
default:
return '未知状态';
}
}
/// 判断订单是否待支付
bool get kr_isPending => kr_status == kr_statusPending;
/// 判断订单是否已支付
bool get kr_isPaid => kr_status == kr_statusPaid;
/// 判断订单是否已取消
bool get kr_isCancelled => kr_status == kr_statusCancelled;
/// 判断订单是否已退款
bool get kr_isRefunded => kr_status == kr_statusRefunded;
/// 判断订单是否支付失败
bool get kr_isFailed => kr_status == kr_statusFailed;
const KROrderStatus({
required this.kr_id,
required this.kr_userId,
required this.kr_orderNo,
required this.kr_type,
required this.kr_quantity,
required this.kr_price,
required this.kr_amount,
required this.kr_giftAmount,
required this.kr_discount,
this.kr_coupon,
required this.kr_couponDiscount,
required this.kr_commission,
required this.kr_method,
required this.kr_feeAmount,
required this.kr_tradeNo,
required this.kr_status,
required this.kr_subscribeId,
this.kr_subscribe,
required this.kr_createdAt,
required this.kr_updatedAt,
});
/// 从JSON映射创建订单状态实例
factory KROrderStatus.fromJson(Map<String, dynamic> json) {
return KROrderStatus(
kr_id: json['id'] as int? ?? 0,
kr_userId: json['user_id'] as int? ?? 0,
kr_orderNo: json['order_no'] as String? ?? '',
kr_type: json['type'] as int? ?? 0,
kr_quantity: json['quantity'] as int? ?? 0,
kr_price: (json['price'] as num?)?.toDouble() ?? 0.0,
kr_amount: (json['amount'] as num?)?.toDouble() ?? 0.0,
kr_giftAmount: (json['gift_amount'] as num?)?.toDouble() ?? 0.0,
kr_discount: (json['discount'] as num?)?.toDouble() ?? 0.0,
kr_coupon: json['coupon'] as String?,
kr_couponDiscount: (json['coupon_discount'] as num?)?.toDouble() ?? 0.0,
kr_commission: (json['commission'] as num?)?.toDouble() ?? 0.0,
kr_method: json['method'] as String? ?? '',
kr_feeAmount: (json['fee_amount'] as num?)?.toDouble() ?? 0.0,
kr_tradeNo: json['trade_no'] as String? ?? '',
kr_status: json['status'] as int? ?? 0,
kr_subscribeId: json['subscribe_id'] as int? ?? 0,
kr_subscribe: json['subscribe'] != null
? KRSubscribe.fromJson(json['subscribe'] as Map<String, dynamic>)
: null,
kr_createdAt: json['created_at'] as int? ?? 0,
kr_updatedAt: json['updated_at'] as int? ?? 0,
);
}
/// 转换为JSON映射
Map<String, dynamic> toJson() {
return {
'id': kr_id,
'user_id': kr_userId,
'order_no': kr_orderNo,
'type': kr_type,
'quantity': kr_quantity,
'price': kr_price,
'amount': kr_amount,
'gift_amount': kr_giftAmount,
'discount': kr_discount,
'coupon': kr_coupon,
'coupon_discount': kr_couponDiscount,
'commission': kr_commission,
'method': kr_method,
'fee_amount': kr_feeAmount,
'trade_no': kr_tradeNo,
'status': kr_status,
'subscribe_id': kr_subscribeId,
'subscribe': kr_subscribe?.toJson(),
'created_at': kr_createdAt,
'updated_at': kr_updatedAt,
};
}
}
/// 订阅信息模型类
class KRSubscribe {
final int kr_id;
final String kr_name;
final String kr_description;
final double kr_unitPrice;
final String kr_unitTime;
final List<KRDiscount> kr_discount;
final int kr_replacement;
final int kr_inventory;
final int kr_traffic;
final int kr_speedLimit;
final int kr_deviceLimit;
final int kr_quota;
final int kr_groupId;
final List<int> kr_serverGroup;
final List<int> kr_server;
final bool kr_show;
final bool kr_sell;
final int kr_sort;
final double kr_deductionRatio;
final bool kr_allowDeduction;
final int kr_resetCycle;
final bool kr_renewalReset;
final int kr_createdAt;
final int kr_updatedAt;
const KRSubscribe({
required this.kr_id,
required this.kr_name,
required this.kr_description,
required this.kr_unitPrice,
required this.kr_unitTime,
required this.kr_discount,
required this.kr_replacement,
required this.kr_inventory,
required this.kr_traffic,
required this.kr_speedLimit,
required this.kr_deviceLimit,
required this.kr_quota,
required this.kr_groupId,
required this.kr_serverGroup,
required this.kr_server,
required this.kr_show,
required this.kr_sell,
required this.kr_sort,
required this.kr_deductionRatio,
required this.kr_allowDeduction,
required this.kr_resetCycle,
required this.kr_renewalReset,
required this.kr_createdAt,
required this.kr_updatedAt,
});
factory KRSubscribe.fromJson(Map<String, dynamic> json) {
return KRSubscribe(
kr_id: json['id'] as int? ?? 0,
kr_name: json['name'] as String? ?? '',
kr_description: json['description'] as String? ?? '',
kr_unitPrice: (json['unit_price'] as num?)?.toDouble() ?? 0.0,
kr_unitTime: json['unit_time'] as String? ?? '',
kr_discount: (json['discount'] as List<dynamic>?)
?.map((e) => KRDiscount.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
kr_replacement: json['replacement'] as int? ?? 0,
kr_inventory: json['inventory'] as int? ?? 0,
kr_traffic: json['traffic'] as int? ?? 0,
kr_speedLimit: json['speed_limit'] as int? ?? 0,
kr_deviceLimit: json['device_limit'] as int? ?? 0,
kr_quota: json['quota'] as int? ?? 0,
kr_groupId: json['group_id'] as int? ?? 0,
kr_serverGroup: (json['server_group'] as List<dynamic>?)
?.map((e) => e as int)
.toList() ?? [],
kr_server: (json['server'] as List<dynamic>?)
?.map((e) => e as int)
.toList() ?? [],
kr_show: json['show'] as bool? ?? false,
kr_sell: json['sell'] as bool? ?? false,
kr_sort: json['sort'] as int? ?? 0,
kr_deductionRatio: (json['deduction_ratio'] as num?)?.toDouble() ?? 0.0,
kr_allowDeduction: json['allow_deduction'] as bool? ?? false,
kr_resetCycle: json['reset_cycle'] as int? ?? 0,
kr_renewalReset: json['renewal_reset'] as bool? ?? false,
kr_createdAt: json['created_at'] as int? ?? 0,
kr_updatedAt: json['updated_at'] as int? ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': kr_id,
'name': kr_name,
'description': kr_description,
'unit_price': kr_unitPrice,
'unit_time': kr_unitTime,
'discount': kr_discount.map((e) => e.toJson()).toList(),
'replacement': kr_replacement,
'inventory': kr_inventory,
'traffic': kr_traffic,
'speed_limit': kr_speedLimit,
'device_limit': kr_deviceLimit,
'quota': kr_quota,
'group_id': kr_groupId,
'server_group': kr_serverGroup,
'server': kr_server,
'show': kr_show,
'sell': kr_sell,
'sort': kr_sort,
'deduction_ratio': kr_deductionRatio,
'allow_deduction': kr_allowDeduction,
'reset_cycle': kr_resetCycle,
'renewal_reset': kr_renewalReset,
'created_at': kr_createdAt,
'updated_at': kr_updatedAt,
};
}
}
/// 折扣信息模型类
class KRDiscount {
final int kr_quantity;
final double kr_discount;
const KRDiscount({
required this.kr_quantity,
required this.kr_discount,
});
factory KRDiscount.fromJson(Map<String, dynamic> json) {
return KRDiscount(
kr_quantity: json['quantity'] as int? ?? 0,
kr_discount: (json['discount'] as num?)?.toDouble() ?? 0.0,
);
}
Map<String, dynamic> toJson() {
return {
'quantity': kr_quantity,
'discount': kr_discount,
};
}
}
+332
View File
@@ -0,0 +1,332 @@
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) {
// 如果是空字符串,直接返回空描述
if (descriptionJson.isEmpty) {
description = KRDescription(kr_features: []);
} else {
try {
description = KRDescription.fromJson(jsonDecode(descriptionJson));
} catch (e) {
KRLogUtil.kr_e('解析描述信息失败: $e');
description = KRDescription(kr_features: []);
}
}
} else if (descriptionJson is Map<String, dynamic>) {
description = KRDescription.fromJson(descriptionJson);
} else {
description = KRDescription(kr_features: []);
}
return KRPackageListItem(
kr_id: json['id'] as int,
kr_name: json['name'] as String,
kr_description: description,
kr_unitPrice: json['unit_price'] ?? 0,
kr_unitTime: json['unit_time'] as String,
kr_discount: discounts,
kr_replacement: json['replacement'] ?? 0,
kr_inventory: json['inventory'] ?? 0,
kr_traffic: json['traffic'] ?? 0,
kr_speedLimit: json['speed_limit'] ?? 0,
kr_deviceLimit: json['device_limit'] ?? 0,
kr_quota: json['quota'] ?? 0,
kr_groupId: json['group_id'] ?? 0,
kr_serverGroup: json['server_group'],
kr_server: json['server'],
kr_show: json['show'] ?? false,
kr_sell: json['sell'] ?? false,
kr_sort: json['sort'] ?? 0,
kr_deductionRatio: json['deduction_ratio'] ?? 0,
kr_allowDeduction: json['allow_deduction'] ?? false,
kr_resetCycle: json['reset_cycle'] ?? 0,
kr_renewalReset: json['renewal_reset'] ?? false,
kr_createdAt: json['created_at'] ?? 0,
kr_updatedAt: json['updated_at'] ?? 0,
);
}
// 获取包含基础选项的完整折扣列表
List<KRDiscount> kr_getCompleteDiscountList() {
// 创建基础选项(数量为1,折扣为100%)
final KRDiscount baseDiscount = KRDiscount(
kr_quantity: 1,
kr_discount: 100, // 折扣为100%,表示原价
);
// 如果原始折扣列表为空,返回只包含基础选项的列表
if (kr_discount.isEmpty) {
return [baseDiscount];
}
// 检查是否已存在数量为1的折扣
final bool hasBaseDiscount = kr_discount.any((discount) => discount.kr_quantity == 1);
// 创建新的列表,包含所有原始折扣
final List<KRDiscount> completeList = List.from(kr_discount);
// 如果没有数量为1的折扣,添加基础选项到列表末尾
if (!hasBaseDiscount) {
completeList.add(baseDiscount);
}
// 按数量排序
completeList.sort((a, b) => a.kr_quantity.compareTo(b.kr_quantity));
return completeList;
}
// 获取折扣后的价格
double kr_getDiscountedPrice() {
if (kr_discount.isEmpty) return kr_unitPrice / 100.0;
final maxDiscount = kr_discount.reduce((a, b) => a.kr_discount > b.kr_discount ? a : b);
return (kr_unitPrice / 100.0) * (maxDiscount.kr_discount / 100.0);
}
// 获取折扣显示文本
String kr_getDiscountDisplay() {
if (kr_discount.isEmpty) return '';
final maxDiscount = kr_discount.reduce((a, b) => a.kr_discount > b.kr_discount ? a : b);
return '${(maxDiscount.kr_discount / 10).toStringAsFixed(1)}';
}
// 获取最大折扣
KRDiscount? kr_getMaxDiscount() {
if (kr_discount.isEmpty) return null;
return kr_discount.reduce((a, b) =>
a.kr_discount > b.kr_discount ? a : b);
}
// 格式化价格显示(保留两位小数)
String kr_formatPrice(double price) {
return price.toStringAsFixed(2);
}
// 获取套餐描述
String kr_getPackageDescription() {
if (kr_discount.isEmpty) {
return '${kr_name} - ${kr_unitPrice / 100.0}元/${kr_unitTime}';
}
final maxDiscount = kr_discount.reduce((a, b) => a.kr_discount > b.kr_discount ? a : b);
return '${kr_name} - ${kr_getDiscountedPrice()}元/${kr_unitTime}';
}
}
class KRDiscount {
// 折扣数量
final int kr_quantity;
// 折扣百分比
final int kr_discount;
KRDiscount({
required this.kr_quantity,
required this.kr_discount,
});
// 从JSON数据创建KRDiscount实例
factory KRDiscount.fromJson(Map<String, dynamic> json) {
// 确保折扣值在 0-100 之间
int discount = json['discount'] ?? 100;
if (discount < 0) discount = 0;
if (discount > 100) discount = 100;
return KRDiscount(
kr_quantity: json['quantity'] ?? 1,
kr_discount: discount,
);
}
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRPaymentMethods {
/// 支付方式列表
final List<KRPaymentMethod> list;
KRPaymentMethods({required this.list});
factory KRPaymentMethods.fromJson(Map<String, dynamic> json) {
final List<dynamic> rawList = json['list'] ?? [];
return KRPaymentMethods(
list: rawList.map((item) => KRPaymentMethod.fromJson(item)).toList(),
);
}
}
class KRPaymentMethod {
final int id;
final String name;
final String platform;
final String icon;
final int feeMode;
final int feePercent;
final int feeAmount;
KRPaymentMethod({
required this.id,
required this.name,
required this.platform,
required this.icon,
required this.feeMode,
required this.feePercent,
required this.feeAmount,
});
factory KRPaymentMethod.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_i(json.toString());
return KRPaymentMethod(
id: (json['id'] ?? 0),
name: json['name'] ?? '',
platform: json['platform'] ?? '',
icon: json['icon'] ?? '',
feeMode: json['fee_mode'] ?? 0,
feePercent: json['fee_percent'] ?? 0,
feeAmount: json['fee_amount'] ?? 0,
);
}
}
+21
View File
@@ -0,0 +1,21 @@
class KRPurchaseOrderNo {
final String orderNo;
KRPurchaseOrderNo({required this.orderNo});
factory KRPurchaseOrderNo.fromJson(Map<String, dynamic> json) {
return KRPurchaseOrderNo(orderNo: json['order_no'] ?? '');
}
}
class KRPurchaseOrderUrl {
final String url;
KRPurchaseOrderUrl({required this.url});
factory KRPurchaseOrderUrl.fromJson(Map<String, dynamic> json) {
return KRPurchaseOrderUrl(url: json['checkout_url'] ?? '');
}
}
+425
View File
@@ -0,0 +1,425 @@
import 'dart:convert';
/// 网站配置信息
class KRSiteConfig {
final KRSiteInfo site;
final KRVerifyConfig verify;
final KRAuthConfig auth;
final KRInviteConfig invite;
final KRCurrencyConfig currency;
final KRSubscribeConfig subscribe;
final KRVerifyCodeConfig verifyCode;
final List<String> oauthMethods;
final bool webAd;
KRSiteConfig({
required this.site,
required this.verify,
required this.auth,
required this.invite,
required this.currency,
required this.subscribe,
required this.verifyCode,
required this.oauthMethods,
required this.webAd,
});
factory KRSiteConfig.fromJson(Map<String, dynamic> json) {
return KRSiteConfig(
site: KRSiteInfo.fromJson(json['site'] ?? {}),
verify: KRVerifyConfig.fromJson(json['verify'] ?? {}),
auth: KRAuthConfig.fromJson(json['auth'] ?? {}),
invite: KRInviteConfig.fromJson(json['invite'] ?? {}),
currency: KRCurrencyConfig.fromJson(json['currency'] ?? {}),
subscribe: KRSubscribeConfig.fromJson(json['subscribe'] ?? {}),
verifyCode: KRVerifyCodeConfig.fromJson(json['verify_code'] ?? {}),
oauthMethods: List<String>.from(json['oauth_methods'] ?? []),
webAd: json['web_ad'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'site': site.toJson(),
'verify': verify.toJson(),
'auth': auth.toJson(),
'invite': invite.toJson(),
'currency': currency.toJson(),
'subscribe': subscribe.toJson(),
'verify_code': verifyCode.toJson(),
'oauth_methods': oauthMethods,
'web_ad': webAd,
};
}
}
/// 站点信息
class KRSiteInfo {
final String host;
final String siteName;
final String siteDesc;
final String siteLogo;
final String keywords;
final String customHtml;
final String customData;
final String crispId;
KRSiteInfo({
required this.host,
required this.siteName,
required this.siteDesc,
required this.siteLogo,
required this.keywords,
required this.customHtml,
required this.customData,
required this.crispId,
});
factory KRSiteInfo.fromJson(Map<String, dynamic> json) {
String crispId = '0';
// 尝试解析 custom_data 中的 kr_website_id
try {
final customDataStr = json['custom_data'] ?? '';
if (customDataStr.isNotEmpty) {
final customDataJson = jsonDecode(customDataStr) as Map<String, dynamic>;
crispId = customDataJson['kr_website_id'] ?? '0';
}
} catch (e) {
// 解析失败时使用默认值
}
return KRSiteInfo(
host: json['host'] ?? '',
siteName: json['site_name'] ?? '',
siteDesc: json['site_desc'] ?? '',
siteLogo: json['site_logo'] ?? '',
keywords: json['keywords'] ?? '',
customHtml: json['custom_html'] ?? '',
customData: json['custom_data'] ?? '',
crispId: crispId,
);
}
Map<String, dynamic> toJson() {
return {
'host': host,
'site_name': siteName,
'site_desc': siteDesc,
'site_logo': siteLogo,
'keywords': keywords,
'custom_html': customHtml,
'custom_data': customData,
};
}
}
/// 验证配置
class KRVerifyConfig {
final String turnstileSiteKey;
final bool enableLoginVerify;
final bool enableRegisterVerify;
final bool enableResetPasswordVerify;
KRVerifyConfig({
required this.turnstileSiteKey,
required this.enableLoginVerify,
required this.enableRegisterVerify,
required this.enableResetPasswordVerify,
});
factory KRVerifyConfig.fromJson(Map<String, dynamic> json) {
return KRVerifyConfig(
turnstileSiteKey: json['turnstile_site_key'] ?? '',
enableLoginVerify: json['enable_login_verify'] ?? false,
enableRegisterVerify: json['enable_register_verify'] ?? false,
enableResetPasswordVerify: json['enable_reset_password_verify'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'turnstile_site_key': turnstileSiteKey,
'enable_login_verify': enableLoginVerify,
'enable_register_verify': enableRegisterVerify,
'enable_reset_password_verify': enableResetPasswordVerify,
};
}
}
/// 认证配置
class KRAuthConfig {
final KRMobileAuth mobile;
final KREmailAuth email;
final KRDeviceAuth device;
final KRRegisterAuth register;
KRAuthConfig({
required this.mobile,
required this.email,
required this.device,
required this.register,
});
factory KRAuthConfig.fromJson(Map<String, dynamic> json) {
return KRAuthConfig(
mobile: KRMobileAuth.fromJson(json['mobile'] ?? {}),
email: KREmailAuth.fromJson(json['email'] ?? {}),
device: KRDeviceAuth.fromJson(json['device'] ?? {}),
register: KRRegisterAuth.fromJson(json['register'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'mobile': mobile.toJson(),
'email': email.toJson(),
'device': device.toJson(),
'register': register.toJson(),
};
}
}
/// 手机号认证配置
class KRMobileAuth {
final bool enable;
final bool enableWhitelist;
final List<String> whitelist;
KRMobileAuth({
required this.enable,
required this.enableWhitelist,
required this.whitelist,
});
factory KRMobileAuth.fromJson(Map<String, dynamic> json) {
return KRMobileAuth(
enable: json['enable'] ?? false,
enableWhitelist: json['enable_whitelist'] ?? false,
whitelist: List<String>.from(json['whitelist'] ?? []),
);
}
Map<String, dynamic> toJson() {
return {
'enable': enable,
'enable_whitelist': enableWhitelist,
'whitelist': whitelist,
};
}
}
/// 邮箱认证配置
class KREmailAuth {
final bool enable;
final bool enableVerify;
final bool enableDomainSuffix;
final String domainSuffixList;
KREmailAuth({
required this.enable,
required this.enableVerify,
required this.enableDomainSuffix,
required this.domainSuffixList,
});
factory KREmailAuth.fromJson(Map<String, dynamic> json) {
return KREmailAuth(
enable: json['enable'] ?? false,
enableVerify: json['enable_verify'] ?? false,
enableDomainSuffix: json['enable_domain_suffix'] ?? false,
domainSuffixList: json['domain_suffix_list'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'enable': enable,
'enable_verify': enableVerify,
'enable_domain_suffix': enableDomainSuffix,
'domain_suffix_list': domainSuffixList,
};
}
}
/// 设备认证配置
class KRDeviceAuth {
final bool enable;
final bool showAds;
final bool enableSecurity;
final bool onlyRealDevice;
KRDeviceAuth({
required this.enable,
required this.showAds,
required this.enableSecurity,
required this.onlyRealDevice,
});
factory KRDeviceAuth.fromJson(Map<String, dynamic> json) {
return KRDeviceAuth(
enable: json['enable'] ?? false,
showAds: json['show_ads'] ?? false,
enableSecurity: json['enable_security'] ?? false,
onlyRealDevice: json['only_real_device'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'enable': enable,
'show_ads': showAds,
'enable_security': enableSecurity,
'only_real_device': onlyRealDevice,
};
}
}
/// 注册认证配置
class KRRegisterAuth {
final bool stopRegister;
final bool enableIpRegisterLimit;
final int ipRegisterLimit;
final int ipRegisterLimitDuration;
KRRegisterAuth({
required this.stopRegister,
required this.enableIpRegisterLimit,
required this.ipRegisterLimit,
required this.ipRegisterLimitDuration,
});
factory KRRegisterAuth.fromJson(Map<String, dynamic> json) {
return KRRegisterAuth(
stopRegister: json['stop_register'] ?? false,
enableIpRegisterLimit: json['enable_ip_register_limit'] ?? false,
ipRegisterLimit: json['ip_register_limit'] ?? 0,
ipRegisterLimitDuration: json['ip_register_limit_duration'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'stop_register': stopRegister,
'enable_ip_register_limit': enableIpRegisterLimit,
'ip_register_limit': ipRegisterLimit,
'ip_register_limit_duration': ipRegisterLimitDuration,
};
}
}
/// 邀请配置
class KRInviteConfig {
final bool forcedInvite;
final double referralPercentage;
final bool onlyFirstPurchase;
KRInviteConfig({
required this.forcedInvite,
required this.referralPercentage,
required this.onlyFirstPurchase,
});
factory KRInviteConfig.fromJson(Map<String, dynamic> json) {
return KRInviteConfig(
forcedInvite: json['forced_invite'] ?? false,
referralPercentage: (json['referral_percentage'] ?? 0).toDouble(),
onlyFirstPurchase: json['only_first_purchase'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'forced_invite': forcedInvite,
'referral_percentage': referralPercentage,
'only_first_purchase': onlyFirstPurchase,
};
}
}
/// 货币配置
class KRCurrencyConfig {
final String currencyUnit;
final String currencySymbol;
KRCurrencyConfig({
required this.currencyUnit,
required this.currencySymbol,
});
factory KRCurrencyConfig.fromJson(Map<String, dynamic> json) {
return KRCurrencyConfig(
currencyUnit: json['currency_unit'] ?? '',
currencySymbol: json['currency_symbol'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'currency_unit': currencyUnit,
'currency_symbol': currencySymbol,
};
}
}
/// 订阅配置
class KRSubscribeConfig {
final bool singleModel;
final String subscribePath;
final String subscribeDomain;
final bool panDomain;
final bool userAgentLimit;
final String userAgentList;
KRSubscribeConfig({
required this.singleModel,
required this.subscribePath,
required this.subscribeDomain,
required this.panDomain,
required this.userAgentLimit,
required this.userAgentList,
});
factory KRSubscribeConfig.fromJson(Map<String, dynamic> json) {
return KRSubscribeConfig(
singleModel: json['single_model'] ?? false,
subscribePath: json['subscribe_path'] ?? '',
subscribeDomain: json['subscribe_domain'] ?? '',
panDomain: json['pan_domain'] ?? false,
userAgentLimit: json['user_agent_limit'] ?? false,
userAgentList: json['user_agent_list'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'single_model': singleModel,
'subscribe_path': subscribePath,
'subscribe_domain': subscribeDomain,
'pan_domain': panDomain,
'user_agent_limit': userAgentLimit,
'user_agent_list': userAgentList,
};
}
}
/// 验证码配置
class KRVerifyCodeConfig {
final int verifyCodeInterval;
KRVerifyCodeConfig({
required this.verifyCodeInterval,
});
factory KRVerifyCodeConfig.fromJson(Map<String, dynamic> json) {
return KRVerifyCodeConfig(
verifyCodeInterval: json['verify_code_interval'] ?? 60,
);
}
Map<String, dynamic> toJson() {
return {
'verify_code_interval': verifyCodeInterval,
};
}
}
+22
View File
@@ -0,0 +1,22 @@
/// 是否注册
class KRStatus {
bool kr_bl= false;
KRStatus({this.kr_bl = false});
KRStatus.fromJson(Map<String, dynamic> json) {
kr_bl = json['Status'] == "true" || json['Status'] == true
? true
: false || json['status'] == "true" || json['status'] == true
? true
: false;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['status'] = kr_bl;
return data;
}
}
+89
View File
@@ -0,0 +1,89 @@
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;
final bool isTryOut; // 试用标志:true=试用,false=付费
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 [],
this.isTryOut = false,
});
factory KRUserAvailableSubscribeItem.fromJson(Map<String, dynamic> json) {
// 从 subscribe 对象中获取订阅信息
final subscribe = json['subscribe'] as Map<String, dynamic>?;
// 时间字段可能是 int (毫秒时间戳) 或 String (ISO 8601)
String convertTime(dynamic value) {
if (value == null) return '';
if (value is String) return value;
if (value is int) {
// 将毫秒时间戳转换为 ISO 8601 字符串
return DateTime.fromMillisecondsSinceEpoch(value).toIso8601String();
}
return value.toString();
}
return KRUserAvailableSubscribeItem(
id: json['id'] as int? ?? 0,
name: subscribe?['name'] as String? ?? '',
deviceLimit: subscribe?['device_limit'] as int? ?? 0,
download: json['download'] as int? ?? 0,
upload: json['upload'] as int? ?? 0,
traffic: json['traffic'] as int? ?? 0,
startTime: convertTime(json['start_time']),
expireTime: convertTime(json['expire_time']),
list: (json['list'] as List<dynamic>?) ?? const [],
isTryOut: subscribe?['is_try_out'] as bool? ?? false,
);
}
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,
'is_try_out': isTryOut,
};
}
}
class KRUserAvailableSubscribeList {
final List<KRUserAvailableSubscribeItem> list;
const KRUserAvailableSubscribeList({
this.list = const [],
});
factory KRUserAvailableSubscribeList.fromJson(Map<String, dynamic> json) {
KRLogUtil.kr_i('订阅json列表: ${json}', tag: 'KRUserAvailableSubscribeList');
final List<dynamic> listData = (json['list'] as List<dynamic>?) ?? const [];
return KRUserAvailableSubscribeList(
list: listData
.map((item) => KRUserAvailableSubscribeItem.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
}
+34
View File
@@ -0,0 +1,34 @@
class KRUserInfo {
final int id;
final String email;
final int refererId;
final String referCode;
final String avatar;
final String areaCode;
final String telephone;
final int balance;
KRUserInfo({
required this.id,
required this.email,
this.refererId = 0,
this.referCode = '',
this.avatar = '',
this.areaCode = '',
this.telephone = '',
this.balance = 0
});
factory KRUserInfo.fromJson(Map<String, dynamic> json) {
return KRUserInfo(
id: json['id'] ?? 0,
email: json['email'] ?? '',
refererId: json['referer_id'] ?? 0,
referCode: json['refer_code'] ?? '',
avatar: json['avatar'] ?? '',
areaCode: json['area_code'] ?? '',
telephone: json['telephone'] ?? '',
balance: json['balance'] ?? 0,
);
}
}
+63
View File
@@ -0,0 +1,63 @@
/// 每日在线时长统计模型
class KRDailyOnlineStat {
final int day;
final String dayName;
final double hours;
KRDailyOnlineStat({
required this.day,
required this.dayName,
required this.hours,
});
factory KRDailyOnlineStat.fromJson(Map<String, dynamic> json) {
return KRDailyOnlineStat(
day: json['day'] ?? 0,
dayName: json['day_name'] ?? '',
hours: (json['hours'] ?? 0.0).toDouble(),
);
}
}
/// 在线时长记录模型
class KROnlineDurationRecord {
final int currentContinuousDays;
final int historyContinuousDays;
final int longestSingleConnection;
KROnlineDurationRecord({
required this.currentContinuousDays,
required this.historyContinuousDays,
required this.longestSingleConnection,
});
factory KROnlineDurationRecord.fromJson(Map<String, dynamic> json) {
return KROnlineDurationRecord(
currentContinuousDays: json['current_continuous_days'] ?? 0,
historyContinuousDays: json['history_continuous_days'] ?? 0,
longestSingleConnection: json['longest_single_connection'] ?? 0,
);
}
}
/// 用户在线时长统计响应模型
class KRUserOnlineDurationResponse {
final List<KRDailyOnlineStat> weeklyStats;
final KROnlineDurationRecord connectionRecords;
KRUserOnlineDurationResponse({
required this.weeklyStats,
required this.connectionRecords,
});
factory KRUserOnlineDurationResponse.fromJson(Map<String, dynamic> json) {
return KRUserOnlineDurationResponse(
weeklyStats: (json['weekly_stats'] as List?)
?.map((e) => KRDailyOnlineStat.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
connectionRecords: KROnlineDurationRecord.fromJson(
json['connection_records'] as Map<String, dynamic>? ?? {},
),
);
}
}
+30
View File
@@ -0,0 +1,30 @@
/// 网页文本内容响应模型
class KRWebText {
/// 隐私政策内容
final String privacyPolicy;
/// 用户协议内容
final String tosContent;
/// 构造函数
KRWebText({
required this.privacyPolicy,
required this.tosContent,
});
/// 从 JSON 创建实例
factory KRWebText.fromJson(Map<String, dynamic> json) {
return KRWebText(
privacyPolicy: json['privacy_policy'] ?? '',
tosContent: json['tos_content'] ?? '',
);
}
/// 转换为 JSON
Map<String, dynamic> toJson() {
return {
'privacy_policy': privacyPolicy,
'tos_content': tosContent,
};
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_country_selector_controller.dart';
class KRCountrySelectorBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRCountrySelectorController>(
() => KRCountrySelectorController(),
);
}
}
@@ -0,0 +1,43 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import '../../../services/singbox_imp/kr_sing_box_imp.dart';
class KRCountrySelectorController extends GetxController {
// 使用 KRCountry 枚举来加载国家
final RxList<KRCountry> kr_countries = <KRCountry>[].obs;
// 当前选中的国家
final Rx<KRCountry> kr_selectedCountry = KRCountry.cn.obs;
@override
void onInit() {
super.onInit();
kr_selectedCountry.value = KRCountryUtil.kr_currentCountry.value;
kr_loadCountries();
}
// 加载国家数据
void kr_loadCountries() {
kr_countries.value = KRCountryUtil.kr_getSupportedCountries();
}
// 选择国家
Future<void> kr_selectCountry(KRCountry country) async {
kr_selectedCountry.value = country;
// try {
// await KRSingBoxImp().kr_updateCountry(country);
// // Get.back();
// } catch (err) {
// }
}
@override
void onClose() {
// TODO: implement onClose
super.onClose();
KRSingBoxImp().kr_updateCountry(kr_selectedCountry.value);
}
}
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_country_util.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../controllers/kr_country_selector_controller.dart';
class KRCountrySelectorView extends GetView<KRCountrySelectorController> {
const KRCountrySelectorView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
// 非渐变色区域
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
),
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_setting.countrySelector,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => ListView.separated(
padding: EdgeInsets.all(16.r),
itemCount: controller.kr_countries.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final country = controller.kr_countries[index];
return _kr_buildCountryCard(country, context);
},
),
),
),
],
),
),
);
}
// 构建国家卡片
Widget _kr_buildCountryCard(KRCountry country, BuildContext context) {
return Obx(
() => InkWell(
onTap: () => controller.kr_selectCountry(country),
child: Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
// 国家图标
KRCountryFlag(
countryCode: country.kr_code,
width: 24.r,
height: 24.r,
),
SizedBox(width: 12.w),
// 国家名称
Text(
KRCountryUtil.kr_getCountryName(country),
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
// 选中标记
if (controller.kr_selectedCountry.value == country)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.r,
),
],
),
),
),
);
}
}
@@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/kr_crisp_controller.dart';
/// Crisp 聊天绑定
class KRCrispBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<KRCrispController>(() => KRCrispController());
}
}
@@ -0,0 +1,178 @@
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 'dart:io' show Platform;
import 'dart:async';
import '../../../utils/kr_device_util.dart';
/// Crisp 聊天控制器
class KRCrispController extends GetxController {
// Crisp 控制器
CrispController? crispController;
// 加载状态
final RxBool kr_isLoading = true.obs;
// 初始化完成状态
final RxBool kr_isInitialized = 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_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.value ?? '';
// 获取设备 ID
final deviceId = await KRDeviceUtil().kr_getDeviceId();
final identifier = userEmail.isNotEmpty ? userEmail : deviceId;
// 根据当前语言设置对应的 Crisp locale
// Crisp 支持的语言:https://docs.crisp.chat/guides/chatbox/languages/
String locale = _getLocaleForCrisp(currentLanguage);
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',
'device_id': deviceId,
});
print('Crisp 初始化完成');
} 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');
}
}
/// 根据应用语言代码获取 Crisp locale
/// 支持应用中所有语言:中文、英文、西班牙语、繁体中文、日语、俄语、爱沙尼亚语
String _getLocaleForCrisp(String languageCode) {
// 映射应用语言到 Crisp 支持的 locale
switch (languageCode) {
case 'zh_CN':
case 'zh':
return 'zh'; // 简体中文
case 'zh_TW':
case 'zhHant':
return 'zh-tw'; // 繁体中文
case 'es':
return 'es'; // 西班牙语
case 'ja':
return 'ja'; // 日语
case 'ru':
return 'ru'; // 俄语
case 'et':
return 'et'; // 爱沙尼亚语
case 'en':
default:
return 'en'; // 英语(默认)
}
}
}
+141
View File
@@ -0,0 +1,141 @@
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 PopScope(
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
await controller.kr_cleanupResources();
}
},
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_buildErrorView(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,
),
),
],
),
);
}
/// 构建错误视图
Widget _kr_buildErrorView(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64.sp,
color: Colors.red,
),
SizedBox(height: 16.sp),
Text(
'客服系统初始化失败',
style: KrAppTextStyle(
fontSize: 16,
color: Colors.red,
),
),
SizedBox(height: 24.sp),
ElevatedButton(
onPressed: () {
controller.kr_initializeCrisp();
},
child: Text('重试'),
),
],
),
);
}
/// 处理返回事件
Future<void> _kr_handleBack() async {
await controller.kr_cleanupResources();
Get.back();
}
/// 处理会话 ID 接收事件
void _kr_onSessionIdReceived(String sessionId) {
debugPrint('Crisp 会话 ID: $sessionId');
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_delete_account_controller.dart';
class KrDeleteAccountBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRDeleteAccountController>(
() => KRDeleteAccountController(),
);
}
}
@@ -0,0 +1,102 @@
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.value;
if (account == null || account.isEmpty) {
KRCommonUtil.kr_showToast('账号不能为空');
return;
}
// 发送验证码(简化后的 API 只需要 email 和 type
final result = await _authApi.kr_sendCode(
account, // 邮箱地址
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;
}
// 删除账号(简化后的 API 只需要 code)
final result = await _authApi.kr_deleteAccount(
kr_codeController.text,
);
result.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
},
(success) {
KRCommonUtil.kr_showToast('删除账号成功');
KRAppRunData.getInstance().kr_loginOut();
},
);
}
}
@@ -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,
),
),
),
),
));
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRDeviceManagementController>(
() => KRDeviceManagementController(),
);
}
}
@@ -0,0 +1,295 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/services/kr_device_info_service.dart';
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'dart:io';
import 'dart:math';
class KRDeviceManagementController extends GetxController {
// 设备列表
final RxList<Map<String, dynamic>> devices = <Map<String, dynamic>>[].obs;
// 加载状态
final RxBool isLoading = true.obs;
// 当前设备ID
String? currentDeviceId;
@override
void onInit() {
super.onInit();
_initDeviceId();
loadDeviceList();
}
/// 初始化当前设备ID
Future<void> _initDeviceId() async {
try {
currentDeviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('当前设备ID: $currentDeviceId', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceManagement');
}
}
/// 加载设备列表
Future<void> loadDeviceList() async {
try {
isLoading.value = true;
KRLogUtil.kr_i('开始加载设备列表', tag: 'DeviceManagement');
// 调用API获取设备列表
final result = await KRUserApi().kr_getUserDevices();
result.fold(
(error) {
KRLogUtil.kr_e('加载设备列表失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, error.msg);
},
(deviceList) {
KRLogUtil.kr_i('获取到 ${deviceList.length} 个设备', tag: 'DeviceManagement');
// 转换设备数据格式
devices.value = deviceList.map((device) {
final identifier = device['identifier']?.toString() ?? '';
final isCurrent = identifier == currentDeviceId;
return {
'id': device['id']?.toString() ?? '',
'identifier': identifier,
'device_name': device['user_agent'] ?? '未知设备',
'ip': device['ip'] ?? '',
'last_login': device['updated_at'] ?? device['created_at'] ?? '',
'is_current': isCurrent,
'enabled': device['enabled'] ?? true,
'online': device['online'] ?? false,
};
}).toList();
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.loadDeviceListFailed);
} finally {
isLoading.value = false;
}
}
/// 删除设备
Future<void> deleteDevice(String id) async {
try {
// 检查是否是本机设备
final device = devices.firstWhere(
(d) => d['id'] == id,
orElse: () => {},
);
if (device.isEmpty) return;
final isCurrent = device['is_current'] ?? false;
// 使用响应式变量来接收确认结果
bool? confirmed;
// 显示确认对话框
await KRDialog.show(
title: AppTranslations.kr_deviceManagement.deleteConfirmTitle,
message: isCurrent
? AppTranslations.kr_deviceManagement.deleteCurrentDeviceMessage
: AppTranslations.kr_deviceManagement.deleteOtherDeviceMessage,
icon: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_rounded,
color: Colors.red,
size: 32,
),
),
confirmText: AppTranslations.kr_dialog.delete,
cancelText: AppTranslations.kr_dialog.kr_cancel,
onConfirm: () {
confirmed = true;
},
onCancel: () {
confirmed = false;
},
);
if (confirmed != true) return;
KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement');
// 调用API解绑设备
final result = await KRUserApi().kr_unbindUserDevice(id);
result.fold(
(error) {
KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(error.msg));
},
(_) async {
KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement');
if (isCurrent) {
// 如果删除的是本机设备,重新进行设备登录
KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement');
// 先关闭当前设备管理页面
Get.back();
// 执行重新登录
await _reloginWithDevice();
} else {
// 删除其他设备,从列表中移除
devices.removeWhere((device) => device['id'] == id);
Get.snackbar(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.deleteSuccess);
}
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(e.toString()));
}
}
/// 重新使用设备登录
Future<void> _reloginWithDevice() async {
try {
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
KRLogUtil.kr_i('开始重新进行设备登录', tag: 'DeviceManagement');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
// 先清除当前的用户信息(但不调用 kr_loginOut,避免显示登录界面)
final appRunData = KRAppRunData.getInstance();
appRunData.kr_isLogin.value = false;
appRunData.kr_token = null;
appRunData.kr_account.value = null;
appRunData.kr_userId.value = null;
// 检查是否启用设备登录
final siteConfigService = KRSiteConfigService();
final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled();
if (!isDeviceLoginEnabled) {
KRLogUtil.kr_w('设备登录未启用,执行完整退出登录', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.tip, AppTranslations.kr_deviceManagement.deviceLoginDisabled);
await appRunData.kr_loginOut();
return;
}
KRLogUtil.kr_i('设备登录已启用,开始调用设备登录接口', tag: 'DeviceManagement');
// 初始化设备信息服务(如果还没初始化)
await KRDeviceInfoService().initialize();
// 调用设备登录接口
final authApi = KRAuthApi();
final result = await authApi.kr_deviceLogin();
result.fold(
(error) {
// 设备登录失败
KRLogUtil.kr_e('设备登录失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.reloginFailed(error.msg));
// 执行完整退出登录,显示登录界面
appRunData.kr_loginOut();
},
(token) async {
// 设备登录成功
KRLogUtil.kr_i('✅ 设备登录成功!', tag: 'DeviceManagement');
KRLogUtil.kr_i('🎫 Token: ${token.substring(0, min(20, token.length))}...', tag: 'DeviceManagement');
// 保存新的用户信息
final deviceId = KRDeviceInfoService().deviceId ?? 'unknown';
await appRunData.kr_saveUserInfo(
token,
'device_$deviceId',
KRLoginType.kr_email,
null,
);
KRLogUtil.kr_i('✅ 设备重新登录成功,已更新用户信息', tag: 'DeviceManagement');
// 等待一小段时间,确保登录状态已经更新
await Future.delayed(const Duration(milliseconds: 300));
// 刷新订阅信息
KRLogUtil.kr_i('🔄 开始刷新订阅信息...', tag: 'DeviceManagement');
try {
await KRSubscribeService().kr_refreshAll();
KRLogUtil.kr_i('✅ 订阅信息刷新成功', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('订阅信息刷新失败: $e', tag: 'DeviceManagement');
}
Get.snackbar(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.reloginSuccess);
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.reloginFailedGeneric);
// 发生异常,执行完整退出登录
await KRAppRunData.getInstance().kr_loginOut();
}
}
/// 获取设备类型和图标
Map<String, dynamic> getDeviceTypeInfo(String userAgent) {
String deviceType = AppTranslations.kr_deviceManagement.deviceTypeUnknown;
String iconName = 'devices';
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeAndroid;
iconName = 'phone_android';
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIos;
iconName = 'phone_iphone';
} else if (userAgent.contains('iPad')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeIpad;
iconName = 'tablet';
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeMacos;
iconName = 'desktop_mac';
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeWindows;
iconName = 'computer';
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
deviceType = AppTranslations.kr_deviceManagement.deviceTypeLinux;
iconName = 'computer';
}
return {
'type': deviceType,
'icon': iconName,
};
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
}
@@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementView extends GetView<KRDeviceManagementController> {
const KRDeviceManagementView({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,
color: Theme.of(context).iconTheme.color,
size: 20.w,
),
onPressed: () => Get.back(),
),
title: Text(
'设备管理',
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
// 内容区域
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(),
);
}
if (controller.devices.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.devices_other,
size: 64.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
SizedBox(height: 16.w),
Text(
'暂无登录设备',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () => controller.loadDeviceList(),
child: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: controller.devices.length,
itemBuilder: (context, index) {
return _buildDeviceItem(
context,
controller.devices[index],
);
},
),
);
}),
),
],
),
),
);
}
/// 构建设备项
Widget _buildDeviceItem(
BuildContext context, Map<String, dynamic> device) {
final id = device['id'] ?? '';
final identifier = device['identifier'] ?? '';
final userAgent = device['device_name'] ?? '未知设备';
final isCurrent = device['is_current'] ?? false;
final ip = device['ip'] ?? '';
final lastLoginRaw = device['last_login'];
final String lastLogin = lastLoginRaw?.toString() ?? '';
// 获取设备类型信息
final deviceInfo = controller.getDeviceTypeInfo(userAgent);
final deviceType = deviceInfo['type'] as String;
final iconName = deviceInfo['icon'] as String;
return Container(
margin: EdgeInsets.only(bottom: 12.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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 设备类型和操作按钮
Row(
children: [
// 设备图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Icon(
_getIconData(iconName),
color: const Color(0xFF1797FF),
size: 24.w,
),
),
SizedBox(width: 12.w),
// 设备信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
deviceType,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
if (isCurrent) ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.w,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
'本机',
style: KrAppTextStyle(
fontSize: 10,
color: Colors.green,
),
),
),
],
],
),
SizedBox(height: 4.w),
Text(
'ID: ${identifier.substring(0, identifier.length > 12 ? 12 : identifier.length)}...',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
// 删除按钮
TextButton(
onPressed: () => controller.deleteDevice(id),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
),
child: Text(
'删除',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
// 分隔线
if (ip.isNotEmpty || lastLogin.isNotEmpty) ...[
SizedBox(height: 12.w),
Divider(height: 1, color: Theme.of(context).dividerColor),
SizedBox(height: 12.w),
],
// 详细信息
if (ip.isNotEmpty)
_buildInfoRow(
context,
'IP地址',
ip,
),
if (ip.isNotEmpty && lastLogin.isNotEmpty) SizedBox(height: 8.w),
if (lastLogin.isNotEmpty)
_buildInfoRow(
context,
'最后登录',
_formatDateTime(lastLoginRaw),
),
],
),
);
}
/// 构建信息行
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Row(
children: [
Text(
'$label: ',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
Expanded(
child: Text(
value,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
/// 格式化时间
String _formatDateTime(dynamic timestamp) {
if (timestamp == null) return '未知';
try {
DateTime dateTime;
if (timestamp is int) {
// 判断是秒级时间戳(10位)还是毫秒级时间戳(13位)
if (timestamp > 9999999999) {
// 毫秒级时间戳
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
} else {
// 秒级时间戳
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
} else if (timestamp is String) {
dateTime = DateTime.parse(timestamp);
} else {
return '未知';
}
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} catch (e) {
return '未知';
}
}
/// 获取图标数据
IconData _getIconData(String iconName) {
switch (iconName) {
case 'phone_android':
return Icons.phone_android;
case 'phone_iphone':
return Icons.phone_iphone;
case 'tablet':
return Icons.tablet_mac;
case 'desktop_mac':
return Icons.desktop_mac;
case 'computer':
return Icons.computer;
default:
return Icons.devices;
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:get/get.dart';
import '../controllers/kr_home_controller.dart';
class KRHomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRHomeController>(
() => KRHomeController(),
);
Get.lazyPut<KRHomeController>(
() => KRHomeController(),
);
}
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
/// 首页基础视图状态枚举
enum KRHomeViewsStatus {
/// 未登录状态
kr_notLoggedIn,
/// 已登录状态
kr_loggedIn,
}
/// 首页列表视图状态枚举
enum KRHomeViewsListStatus {
kr_none,
kr_loading,
kr_error,
kr_serverList,
kr_countrySubscribeList,
kr_serverSubscribeList,
kr_subscribeList,
}
+203
View File
@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../localization/app_translations.dart';
import '../../../widgets/kr_app_text_style.dart';
import '../../../widgets/kr_loading_animation.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'kr_home_connection_info_view.dart';
import 'kr_home_connection_options_view.dart';
import 'kr_home_node_list_view.dart';
import '../widgets/kr_subscription_card.dart';
import 'kr_home_trial_card.dart';
import 'kr_home_last_day_card.dart';
import '../../../utils/kr_log_util.dart';
class KRHomeBottomPanel extends GetView<KRHomeController> {
const KRHomeBottomPanel({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
final currentStatus = controller.kr_currentListStatus.value;
KRLogUtil.kr_i('构建底部面板', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前视图状态: ${controller.kr_currentViewStatus.value}',
tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
if (controller.kr_currentListStatus.value ==
KRHomeViewsListStatus.kr_loading) {
return _kr_buildLoadingView();
}
if (controller.kr_currentListStatus.value ==
KRHomeViewsListStatus.kr_error) {
return _kr_buildErrorView(context);
}
if (currentStatus == KRHomeViewsListStatus.kr_serverList ||
currentStatus == KRHomeViewsListStatus.kr_countrySubscribeList ||
currentStatus == KRHomeViewsListStatus.kr_serverSubscribeList ||
currentStatus == KRHomeViewsListStatus.kr_subscribeList) {
return const KRHomeNodeListView();
}
return _kr_buildDefaultView(context);
});
}
Widget _kr_buildDefaultView(BuildContext context) {
// 使用 GetX 的 .obs 变量来避免重复访问
final hasValidSubscription =
controller.kr_subscribeService.kr_currentSubscribe.value != null;
final isTrial = controller.kr_subscribeService.kr_isTrial;
final isLastDay = controller.kr_subscribeService.kr_isLastDayOfSubscription;
final isNotLoggedIn = controller.kr_currentViewStatus.value ==
KRHomeViewsStatus.kr_notLoggedIn;
KRLogUtil.kr_i('构建默认视图', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否未登录: $isNotLoggedIn', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否有有效订阅: $hasValidSubscription', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('是否试用: ${isTrial.value}', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 主要内容区域
if (isNotLoggedIn)
// 未登录状态下,使用 SingleChildScrollView 让内容自然撑开
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
child: const KRHomeConnectionOptionsView(),
),
],
),
)
else
// 已登录状态下,使用固定高度
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 1. 如果已订阅,展示当前连接卡片
if (hasValidSubscription)
Container(
margin: EdgeInsets.only(top: 12.h),
child: const KRHomeConnectionInfoView())
else
Container(
margin:
EdgeInsets.only(top: 12.h, left: 12.w, right: 12.w),
child: const KRSubscriptionCard()),
// 2. 如果已订阅且是试用,展示试用卡片
if (hasValidSubscription && isTrial.value)
Container(
margin: EdgeInsets.only(top: 12.h),
child: const KRHomeTrialCard(),
),
// 3. 如果已订阅且是最后一天,展示最后一天卡片
if (hasValidSubscription && isLastDay.value && !isTrial.value)
Container(
margin: EdgeInsets.only(top: 12.h),
child: const KRHomeLastDayCard(),
),
// 4. 连接选项(分组和国家入口)
Padding(
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
child: const KRHomeConnectionOptionsView(),
),
],
),
),
),
],
);
}
Widget _kr_buildLoadingView() {
KRLogUtil.kr_i('构建加载视图', tag: 'HomeBottomPanel');
KRLogUtil.kr_i('当前高度: ${controller.kr_bottomPanelHeight.value}',
tag: 'HomeBottomPanel');
return Center(
child: CircularProgressIndicator(
color: Colors.green,
strokeWidth: 2.0,
),
);
}
Widget _kr_buildErrorView(BuildContext context) {
return Container(
height: 200.w,
padding: EdgeInsets.all(16.w),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 48.w,
color: Theme.of(context).colorScheme.error,
),
SizedBox(height: 16.w),
Text(
AppTranslations.kr_home.error,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 8.w),
Text(
AppTranslations.kr_home.checkNetwork,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 24.w),
SizedBox(
width: 200.w,
height: 44.h,
child: ElevatedButton(
onPressed: () => controller.kr_refreshAll(),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
AppTranslations.kr_home.retry,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,227 @@
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 '--';
} else if (delay == -1) {
return AppTranslations.kr_home.connecting;
} else if (delay == 0) {
return AppTranslations.kr_home.connected;
} else if (delay >= 3000) {
return AppTranslations.kr_home.timeout;
} else {
return '${delay}ms';
}
}
// 🔧 修复:只有 delay == -1 时才显示 connecting 动画
if (delay == -1) {
return Row(
children: [
KRSimpleLoading(
color: Colors.green,
size: 12.w,
duration: const Duration(milliseconds: 800),
),
SizedBox(width: 2.w),
Text(
AppTranslations.kr_home.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,96 @@
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 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.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),
_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) {
// 未订阅状态下,使用统一的订阅导航工具
KRSubscribeNavigationUtil.navigateToPurchase(tag: 'ConnectionOptions');
} 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,
),
],
),
],
),
),
);
}
}
+150
View File
@@ -0,0 +1,150 @@
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 '../../../utils/kr_subscribe_navigation_util.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: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'LastDayCard'),
child: Row(
children: [
Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Colors.blue,
),
],
),
),
],
),
// 倒计时显示
SizedBox(height: 10.w),
Row(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.timer_outlined,
color: Colors.blue,
size: 16.w,
),
SizedBox(width: 4.w),
Text(
AppTranslations.kr_home.lastDaySubscriptionMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() {
final isLastDay =
controller.kr_subscribeService.kr_isLastDayOfSubscription.value;
final remainingTime = controller.kr_subscribeService.kr_subscriptionRemainingTime.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
remainingTime,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isLastDay
? (DateTime.now().millisecondsSinceEpoch %
2000 <
1000
? Colors.red
: Colors.blue)
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
SizedBox(height: 4.w),
Text(
AppTranslations.kr_home.subscriptionEndMessage,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
),
],
);
}),
],
),
),
],
),
],
),
),
);
}
}
+912
View File
@@ -0,0 +1,912 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_country_flag.dart';
import '../../../model/business/kr_outbound_item.dart';
import '../../../utils/kr_log_util.dart';
import '../controllers/kr_home_controller.dart';
import '../models/kr_home_views_status.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/widgets/kr_network_image.dart';
import '../../../widgets/kr_simple_loading.dart';
import '../../../../singbox/model/singbox_proxy_type.dart';
/// 节点列表视图组件
/// 用于展示所有节点相关的列表视图
class KRHomeNodeListView extends GetView<KRHomeController> {
const KRHomeNodeListView({super.key});
// 添加常量定义
static const Color krModernGreen = Color(0xFF4CAF50);
static const Color krModernGreenLight = Color(0xFF81C784);
// 存储每个节点的随机延迟值(仅用于界面显示)
static final Map<String, int> _fakeDelays = {};
/// 获取显示的延迟值
int _getDisplayDelay(KRHomeController controller, KROutboundItem item) {
// 如果已连接,使用真实的延迟值
if (controller.kr_isConnected.value) {
return item.urlTestDelay.value;
}
// 如果未连接,使用随机延迟值
if (!_fakeDelays.containsKey(item.tag)) {
// 生成30ms-100ms之间的随机延迟
final random = Random();
_fakeDelays[item.tag] = 30 + random.nextInt(71); // 30 + (0-70) = 30-100ms
}
return _fakeDelays[item.tag] ?? 0;
}
@override
Widget build(BuildContext context) {
return Obx(() {
// 根据列表状态选择不同的视图
switch (controller.kr_currentListStatus.value) {
case KRHomeViewsListStatus.kr_serverList:
return _buildServerList(context);
case KRHomeViewsListStatus.kr_subscribeList:
return _buildSubscribeList(context);
case KRHomeViewsListStatus.kr_countrySubscribeList:
return _kr_buildRegionList(context);
case KRHomeViewsListStatus.kr_serverSubscribeList:
return _kr_buildServerSubscribeList(context);
default:
return const SizedBox.shrink();
}
});
}
/// 服务器列表视图
/// 构建专用服务器列表
Widget _buildServerList(BuildContext context) {
return Container(
width: ScreenUtil().screenWidth,
height: 360.w, // 减小高度比例
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.w),
topRight: Radius.circular(20.w),
),
),
child: Column(
children: [
// 标题栏
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_home.serverListTitle,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
GestureDetector(
onTap: () {
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Icon(
Icons.close,
size: 24.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
// 列表内容
Expanded(
child: Obx(() {
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noServers,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w),
itemCount: controller.kr_subscribeService.groupOutboundList.length,
itemBuilder: (context, index) {
final group =
controller.kr_subscribeService.groupOutboundList[index];
return Container(
margin: EdgeInsets.only(bottom: 8.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.1),
width: 1.w,
),
),
child: InkWell(
onTap: () {
controller.kr_setCurrentGroup(group);
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_serverSubscribeList;
},
borderRadius: BorderRadius.circular(12.w),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
KRNetworkImage(
kr_imageUrl: group.icon,
kr_width: 32.w,
kr_height: 32.w,
kr_fit: BoxFit.cover,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
group.tag,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
),
);
},
);
}),
),
],
),
);
}
/// 国家订阅列表视图
Widget _kr_buildRegionList(BuildContext context) {
return _kr_buildListPage(
context,
title: AppTranslations.kr_home.countryListTitle,
listContent: Obx(() {
if (controller.kr_subscribeService.groupOutboundList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noRegions,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return _kr_buildListContainer(
context,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(16.w, 8.w, 16.w, 0),
itemCount:
controller.kr_subscribeService.countryOutboundList.length,
itemBuilder: (context, index) {
final country =
controller.kr_subscribeService.countryOutboundList[index];
return Column(
children: [
// 主区域
InkWell(
onTap: () {
country.isExpand.value = !country.isExpand.value;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.w),
child: Row(
children: [
KRCountryFlag(
countryCode: country.country,
width: 40.w,
height: 40.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller
.kr_getCountryFullName(country.country),
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
),
],
),
),
Obx(() {
return Icon(
country.isExpand.value
? Icons.keyboard_arrow_down
: Icons.arrow_forward_ios,
size: 16.w,
color:
Theme.of(context).textTheme.bodySmall?.color,
);
}),
],
),
),
),
// 展开的服务器列表
Obx(() {
final isExpanded = country.isExpand.value;
if (!isExpanded) return const SizedBox();
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.w),
itemCount: country.outboundList.length,
itemBuilder: (context, index) {
final server = country.outboundList[index];
return Column(
children: [
InkWell(
onTap: () {
print(server.tag);
KRSingBoxImp.instance
.kr_selectOutbound(server.tag);
controller.kr_selectNode(server.tag);
// 添加状态切换,回到默认状态
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.w,
horizontal: 16.w,
),
decoration: BoxDecoration(
// 添加轻微的背景色以区分点击区域
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8.w),
),
child: _kr_buildNodeListItem(
context,
item: server,
),
),
),
// 添加分隔线
if (index < country.outboundList.length - 1)
Divider(
height: 1.w,
indent: 16.w,
endIndent: 16.w,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
],
);
},
);
}),
Divider(
height: 1.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
);
},
),
);
}),
);
}
/// 服务器订阅列表视图
// 修改服务器订阅列表视图
Widget _kr_buildServerSubscribeList(BuildContext context) {
return _kr_buildListPage(
context,
title: controller.kr_currentGroup.value?.tag ?? '',
onBack: () => controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_serverList,
listContent: Obx(() {
final servers = controller.kr_currentGroup.value?.outboundList ?? [];
if (servers.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noNodes,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
return _kr_buildListContainer(
context,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(16.w, 16.w, 16.w, 0),
itemCount: servers.length,
itemBuilder: (context, index) {
final server = servers[index];
return Column(
children: [
InkWell(
onTap: () {
KRLogUtil.kr_i(server.tag);
KRSingBoxImp.instance.kr_selectOutbound(server.tag);
controller.kr_selectNode(server.tag);
// 添加状态切换,回到默认状态
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4.w),
child: _kr_buildNodeListItem(
context,
item: server,
),
),
),
if (index < servers.length - 1)
Divider(
height: 1.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
],
);
},
),
);
}),
);
}
Widget _kr_buildListPage(
BuildContext context, {
required String title,
VoidCallback? onBack,
required Widget listContent,
}) {
return Container(
width: ScreenUtil().screenWidth,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.w),
topRight: Radius.circular(20.w),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_kr_buildTitleBar(
context,
title: title,
onBack: onBack,
onClose: () =>
controller.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none,
),
SizedBox(height: 16.w),
Expanded(
child: Column(
children: [
Expanded(child: listContent),
// 添加底部间距
SizedBox(height: 12.w),
],
),
),
],
),
);
}
// 抽取公共的标题栏组件
Widget _kr_buildTitleBar(
BuildContext context, {
required String title,
VoidCallback? onBack,
VoidCallback? onClose,
}) {
return Padding(
padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
if (onBack != null) ...[
GestureDetector(
onTap: onBack,
child: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(width: 8.w),
],
Text(
title,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
if (onClose != null)
GestureDetector(
onTap: onClose,
child: Icon(
Icons.close,
size: 24.w,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
/// 构建列表容器
Widget _kr_buildListContainer(
BuildContext context, {
required Widget child,
}) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: child,
);
}
/// 构建节点列表项
Widget _kr_buildNodeListItem(
BuildContext context, {
required KROutboundItem item,
}) {
// 获取延迟颜色
Color getLatencyColor(int delay) {
if (delay == 0) {
return Colors.transparent;
} else if (delay < 500) {
return krModernGreen;
} else if (delay < 3000) {
return Color(0xFFFFB700); // 使用更容易看清的黄色
} else {
return Colors.red;
}
}
// 获取图标颜色
Color? getIconColor(int delay) {
if (delay == 0) {
return null;
} else if (delay >= 3000) {
return Colors.red;
}
return null;
}
return Container(
key: ValueKey(item.id),
padding: EdgeInsets.symmetric(vertical: 8.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: "home_list_location",
width: 36.w,
height: 36.w,
color: getIconColor(item.urlTestDelay.value),
),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
item.tag,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Obx(
() => controller.kr_cutTag.value == item.tag
? Container(
margin: EdgeInsets.only(left: 4.w),
padding: EdgeInsets.symmetric(
horizontal: 4.w, vertical: 1.w),
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
),
)
: const SizedBox.shrink(),
),
],
),
SizedBox(height: 2.w),
Text(
item.city,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
// 显示延迟速度
GetBuilder<KRHomeController>(
id: item.tag,
builder: (controller) {
// 获取显示的延迟值
int displayDelay = _getDisplayDelay(controller, item);
return Container(
alignment: Alignment.center,
child: Text(
displayDelay == 0
? ''
: displayDelay >= 3000
? AppTranslations.kr_home.timeout
: '${displayDelay}ms',
style: KrAppTextStyle(
fontSize: 12,
color: getLatencyColor(displayDelay),
),
),
);
},
),
],
),
);
}
// 修改订阅列表视图
Widget _buildSubscribeList(BuildContext context) {
return _kr_buildListPage(
context,
title: AppTranslations.kr_home.nodeListTitle,
listContent: Obx(() {
if (controller.kr_subscribeService.allList.isEmpty) {
return Center(
child: Text(
AppTranslations.kr_home.noNodes,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
// 自动触发延迟测试(仅在未连接状态下)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!controller.kr_isConnected.value && !controller.kr_isLatency.value) {
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'NodeListView');
controller.kr_urlTest();
}
});
return _kr_buildListContainer(
context,
child: ListView(
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0),
children: [
// 延迟测试按钮作为第一个列表项
InkWell(
onTap: () => controller.kr_urlTest(),
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.w),
margin: EdgeInsets.only(top: 8.w), // 添加上方间距
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 36.w,
height: 36.w,
decoration: BoxDecoration(
color: krModernGreenLight.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: controller.kr_isLatency.value
? KRSimpleLoading(
color: krModernGreen,
size: 24.w,
duration: const Duration(milliseconds: 800),
)
: Icon(
Icons.speed,
size: 24.w,
color: krModernGreen,
),
),
),
SizedBox(width: 8.w),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.kr_isLatency.value
? AppTranslations.kr_home.testing
: AppTranslations.kr_home.testLatency,
style: KrAppTextStyle(
fontSize: 14,
color: controller.kr_isLatency.value
? Theme.of(context)
.textTheme
.bodySmall
?.color
: Theme.of(context)
.textTheme
.bodyMedium
?.color,
fontWeight: controller.kr_isLatency.value
? FontWeight.normal
: FontWeight.w500,
),
),
if (!controller.kr_isLatency.value) ...[
SizedBox(height: 2.w),
Text(
AppTranslations.kr_home.refreshLatencyDesc,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
),
],
],
),
),
if (!controller.kr_isLatency.value)
Icon(
Icons.arrow_forward_ios,
size: 12.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
],
),
),
),
// 分隔线
Divider(
height: 16.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// Auto 选项
InkWell(
onTap: () {
controller.kr_selectNode('auto');
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KrLocalImage(
imageName: "home_list_location",
width: 36.w,
height: 36.w,
color: controller.kr_cutTag.value == 'auto'
? Colors.green
: null,
),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
AppTranslations.kr_home.autoSelect,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (controller.kr_cutTag.value == 'auto')
Container(
margin: EdgeInsets.only(left: 4.w),
padding: EdgeInsets.symmetric(
horizontal: 4.w, vertical: 1.w),
decoration: BoxDecoration(
color:
krModernGreenLight.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
AppTranslations.kr_home.selected,
style: KrAppTextStyle(
fontSize: 10,
color: krModernGreen,
fontWeight: FontWeight.w500,
),
),
),
],
),
SizedBox(height: 2.w),
Obx(() {
// 获取当前自动选择的节点
String selectedNode =
AppTranslations.kr_home.autoSelect;
int delay = 0;
for (var group
in KRSingBoxImp.instance.kr_activeGroups) {
if (group.type == ProxyType.urltest) {
selectedNode = group.selected;
delay = controller
.kr_subscribeService.keyList[group.selected]
?.urlTestDelay.value ??
0;
break;
}
}
return Text(
selectedNode,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context)
.textTheme
.bodySmall
?.color,
),
);
}),
],
),
),
Obx(() {
// 获取当前自动选择的节点
String selectedNode =
AppTranslations.kr_home.autoSelect;
int delay = 0;
for (var group
in KRSingBoxImp.instance.kr_activeGroups) {
if (group.type == ProxyType.urltest) {
selectedNode = group.selected;
delay = controller
.kr_subscribeService
.keyList[group.selected]
?.urlTestDelay
.value ??
0;
break;
}
}
return delay > 0
? Container(
alignment: Alignment.center,
child: Text(
delay < 3000
? '${delay}ms'
: AppTranslations.kr_home.timeout,
style: KrAppTextStyle(
fontSize: 12,
color: delay < 3000
? Colors.green
: Colors.red,
),
),
)
: const SizedBox.shrink();
}),
],
),
),
),
// 分隔线
Divider(
height: 16.w,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// 节点列表
...controller.kr_subscribeService.allList
.map((node) => Column(
children: [
InkWell(
onTap: () {
KRLogUtil.kr_i(node.tag);
KRSingBoxImp.instance.kr_selectOutbound(node.tag);
controller.kr_selectNode(node.tag);
controller.kr_currentListStatus.value =
KRHomeViewsListStatus.kr_none;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 4.w),
child: _kr_buildNodeListItem(
context,
item: node,
),
),
),
if (node !=
controller.kr_subscribeService.allList.last)
Divider(
height: 1.w,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
],
))
.toList(),
],
),
);
}),
);
}
}
@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:math';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
class KRHomeSubscriptionView extends GetView<KRHomeController> {
const KRHomeSubscriptionView({super.key});
@override
Widget build(BuildContext context) {
return Obx(() {
if (!KRAppRunData().kr_isLogin.value) {
return const SizedBox.shrink();
}
final currentSubscribe =
controller.kr_subscribeService.kr_currentSubscribe.value;
if (currentSubscribe == null) {
return const SizedBox.shrink();
}
KRLogUtil.kr_i('当前订阅名称: ${currentSubscribe.name}',
tag: 'SubscriptionView');
final totalTraffic = currentSubscribe.traffic;
final usedTraffic = currentSubscribe.download + currentSubscribe.upload;
final hasTrafficLimit = totalTraffic > 0;
var trafficPercentage =
hasTrafficLimit ? (usedTraffic / totalTraffic).clamp(0.0, 1.0) : 0.0;
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bolt_rounded,
size: 14.w,
color: Theme.of(context).brightness == Brightness.light
? Colors.black54
: Colors.white54,
),
SizedBox(width: 4.w),
Expanded(
child: Text(
currentSubscribe.name,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 4.w),
Container(
height: 3.h,
width: 20.w,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[200]
: Colors.grey[800],
borderRadius: BorderRadius.circular(1.5.r),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: trafficPercentage,
child: Container(
decoration: BoxDecoration(
color: _getTrafficColor(trafficPercentage),
borderRadius: BorderRadius.circular(1.5.r),
),
),
),
),
SizedBox(width: 4.w),
Icon(
Icons.swap_horiz,
size: 14.w,
color: Theme.of(context).brightness == Brightness.light
? Colors.black.withOpacity(0.5)
: Colors.white.withOpacity(0.5),
),
],
),
);
});
}
Widget _buildLoadingView(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.grey[900]!,
Theme.of(context).brightness == Brightness.light
? Colors.grey[50]!
: Colors.grey[800]!,
],
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
const SizedBox(width: 16),
Text(
AppTranslations.kr_home.loading,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.light
? Colors.black
: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
IconData _getStatusIcon(KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return Icons.error_outline_rounded;
} else if (difference.inDays <= 1) {
return Icons.warning_amber_rounded;
} else {
return Icons.check_circle_outline_rounded;
}
}
Color _getStatusColor(
BuildContext context, KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return Colors.red;
} else if (difference.inDays <= 1) {
return Colors.orange;
} else {
return const Color(0xFF00E52B);
}
}
String _getStatusText(KRUserAvailableSubscribeItem subscribe) {
final now = DateTime.now();
final expireTime = DateTime.parse(subscribe.expireTime);
final difference = expireTime.difference(now);
if (difference.isNegative) {
return '已过期';
} else if (difference.inDays <= 1) {
return '即将到期';
} else {
return '有效';
}
}
Color _getTrafficColor(double percentage) {
if (percentage >= 0.9) {
return Colors.red;
} else if (percentage >= 0.7) {
return Colors.orange;
} else {
return const Color(0xFF00E52B);
}
}
String _formatTraffic(int bytes) {
if (bytes < 1024) {
return '$bytes B';
} else if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
} catch (e) {
return dateStr;
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../routes/app_pages.dart';
import '../../../services/kr_subscribe_service.dart';
import '../../../utils/kr_log_util.dart';
import '../../../utils/kr_subscribe_navigation_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: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'TrialCard'),
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,
),
),
],
),
);
});
}
}
+330
View File
@@ -0,0 +1,330 @@
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(),
// 登录视图
Align(
alignment: Alignment.bottomCenter,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Container(
width: double.infinity,
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;
}
}
}
+529
View File
@@ -0,0 +1,529 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import '../../../utils/kr_fm_tc.dart';
import '../controllers/kr_home_controller.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 GetBuilder<KRHomeController>(
id: 'map_markers', // ① 指定唯一ID
builder: (controller) {
return FlutterMap(
mapController: controller.kr_mapController,
options: MapOptions(
initialCenter: _kr_getInitialMapCenter(),
initialZoom: 2.0,
minZoom: 2,
maxZoom: 5,
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;
// 地图移动结束后刷新标记
controller.showMarkersMap();
}
}
} catch (e) {
KRLogUtil.kr_e('地图事件处理失败: $e', tag: 'HomeMapView');
}
},
keepAlive: true,
interactionOptions: InteractionOptions(
enableMultiFingerGestureRace: true,
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
_kr_buildTileLayer(context),
_kr_buildMarkers(context),
],
);
},
);
}
/// 构建地图瓦片层
Widget _kr_buildTileLayer(BuildContext context) {
return TileLayer(
tileProvider: AssetTileProvider(),
urlTemplate: KRFMTC.kr_getTileUrl(),
subdomains: KRFMTC.kr_getTileSubdomains(),
userAgentPackageName: 'app.baertw123.com',//app.brAccelerator.com
/* tileProvider: KRFMTC.kr_getTileProvider(),*/
tileBuilder: (context, child, tileImage) {
return child;
},
);
}
MarkerLayer _kr_buildMarkers(BuildContext content){
int zoom = 2;
try {
zoom = controller.kr_mapController.camera.zoom.toInt();
} catch (e) {
KRLogUtil.kr_e('获取地图缩放级别失败: $e', tag: 'HomeMapView');
}
// 详细调试信息
KRLogUtil.kr_i('========== 地图标记调试信息 ==========', tag: 'HomeMapView');
KRLogUtil.kr_i('当前地图缩放级别: $zoom', tag: 'HomeMapView');
KRLogUtil.kr_i('节点总数: ${controller.kr_subscribeService.allList.length}', tag: 'HomeMapView');
KRLogUtil.kr_i('国家分组数: ${controller.kr_subscribeService.countryOutboundList.length}', tag: 'HomeMapView');
// 根据缩放级别显示不同的标记
if (zoom < 5) {
// 低缩放级别:显示国家级聚合标记
KRLogUtil.kr_i('使用国家聚合模式,显示 ${controller.kr_subscribeService.countryOutboundList.length} 个国家标记', tag: 'HomeMapView');
// 打印前3个国家的信息
for (int i = 0; i < 3 && i < controller.kr_subscribeService.countryOutboundList.length; i++) {
final country = controller.kr_subscribeService.countryOutboundList[i];
KRLogUtil.kr_i('国家[$i]: ${country.country}, 节点数: ${country.outboundList.length}', tag: 'HomeMapView');
}
return MarkerLayer(
markers: controller.kr_subscribeService.countryOutboundList
.map((item) => _buildStyledProvMarker(item))
.toList(),
);
} else {
// 高缩放级别:显示单个节点标记
KRLogUtil.kr_i('使用单节点模式,显示 ${controller.kr_subscribeService.allList.length} 个节点标记', tag: 'HomeMapView');
// 打印前3个节点的信息
for (int i = 0; i < 3 && i < controller.kr_subscribeService.allList.length; i++) {
final node = controller.kr_subscribeService.allList[i];
KRLogUtil.kr_i('节点[$i]: ${node.tag}, 位置: (${node.latitude}, ${node.longitude})', tag: 'HomeMapView');
}
return MarkerLayer(
markers: controller.kr_subscribeService.allList
.map((item) => _buildStyledMarker(item))
.toList(),
);
}
}
/// 构建国家级聚合标记
Marker _buildStyledProvMarker(dynamic node) {
double z = controller.kr_mapController.camera.zoom;
double lat = 0.0;
double lng = 0.0;
String id = "";
int len = -1;
if (node.outboundList.isNotEmpty) {
var f = node.outboundList.first;
len = node.outboundList.length;
id = f.id;
lat = f.latitudeCountry;
lng = f.longitudeCountry;
KRLogUtil.kr_i('构建国家标记: ${f.country}', tag: 'HomeMapView');
KRLogUtil.kr_i(' - latitudeCountry: $lat', tag: 'HomeMapView');
KRLogUtil.kr_i(' - longitudeCountry: $lng', tag: 'HomeMapView');
KRLogUtil.kr_i(' - 节点latitude: ${f.latitude}', tag: 'HomeMapView');
KRLogUtil.kr_i(' - 节点longitude: ${f.longitude}', tag: 'HomeMapView');
KRLogUtil.kr_i(' - 节点数量: $len', tag: 'HomeMapView');
// 如果国家坐标为0,使用节点坐标
if (lat == 0.0 && lng == 0.0) {
lat = f.latitude;
lng = f.longitude;
KRLogUtil.kr_w('国家坐标为0,使用节点坐标: ($lat, $lng)', tag: 'HomeMapView');
}
}
double fontSize = 8;
double radius = 0 + len * 10 + z * 10;
double radius2 = 20;
if (z < 3) {
fontSize = 8;
radius = 0 + len * 10 + z * 10;
radius2 = 15;
} else if (z < 4) {
fontSize = 8;
radius = 30 + len * 10 + z * 10;
radius2 = 20;
} else if (z < 5) {
fontSize = 10;
radius = 70 + len * 10 + z * 10;
radius2 = 20;
} else if (z < 6) {
fontSize = 12;
radius = 100 + len * 10 + z * 10;
radius2 = 30;
}
return Marker(
key: ValueKey('country_$id'),
point: LatLng(lat, lng),
width: radius,
height: radius,
child: GestureDetector(
onTap: () {
// 点击国家标记时,放大地图到该位置
KRLogUtil.kr_i('点击国家标记,放大到: lat=$lat lng=$lng', tag: 'HomeMapView');
controller.kr_moveToLocation(LatLng(lat, lng), 5.0);
},
child: _buildDoubleCircle(radius, radius2, Colors.blue, len, fontSize),
),
);
}
/// 构建样式化的标记
Marker _buildStyledMarker(dynamic node) {
print("原始Marker${node}");
int type = 0;
MaterialColor markerColor = Colors.grey;
//如果没订阅过,就是灰色
bool status = controller.kr_isConnected.value;
print("连接状态:$status");
if(status){
if (node.urlTestDelay.value == 0) {
// 延迟为0时使用默认颜色
type=0;
markerColor = Colors.blue;
} else if (node.urlTestDelay.value < 500) {
// 延迟小于500ms显示绿色
type=1;
markerColor = Colors.green;
} else if (node.urlTestDelay.value < 3000) {
// 延迟小于3000ms显示黄色
type=2;
markerColor = Colors.yellow;
} else if (node.urlTestDelay.value > 3000 ){
// 超时显示红色
type=3;
markerColor = Colors.red;
}
}else{
type=4;
markerColor = Colors.blue;
}
if(node.selected==1){
if(type==1||type==0){//只有绿色才有雷达效果
return RadarMarker(
key: ValueKey('key_${node.id}'),
point: LatLng(node.latitude, node.longitude),
radarSize1:75,
radarSize2:37,
radarSize3: 25.0,
innerColor: Colors.green,
outerColor: Colors.green,
animationDuration: const Duration(seconds: 4),
onTap: () => _onMarkerTap(node),
).toMarker();
}else{ //蓝色. 灰色 黄色
return Marker(
key: ValueKey('key_${node.id}'),
point: LatLng(node.latitude, node.longitude),
width: 150,
height: 150.0,
child: GestureDetector(
onTap: () => _onMarkerTap(node),
child: _buildDoubleCircle(150,20, markerColor, -1,14),
),
);
}
}else{
return Marker(
key: ValueKey('key_${node.id}'),
point: LatLng(node.latitude, node.longitude),
width: 30.0,
height: 30.0,
child: GestureDetector(
onTap: () => _onMarkerTap(node),
child: _buildDoubleCircle(30,10, markerColor, -1,14),
),
);
}
/*
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,
);
},
),
);*/
}
/// 处理标记点击事件
void _onMarkerTap(dynamic node) {
try {
KRLogUtil.kr_i('点击节点: ${node.tag}', tag: 'HomeMapView');
// 选择该节点
controller.kr_selectNode(node.tag);
// 如果有底部面板控制器,将面板收起到 30% 高度
if (controller.kr_sheetController.isAttached) {
controller.kr_sheetController.animateTo(
0.3,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
} catch (e) {
KRLogUtil.kr_e('处理标记点击失败: $e', tag: 'HomeMapView');
}
}
/// 获取初始地图中心点
LatLng _kr_getInitialMapCenter() {
if (controller.kr_isUserMoving.value) {
return controller.kr_lastMapCenter.value;
}
if (controller.kr_cutSeletedTag.isEmpty) {
return const LatLng(5.0, 105.0); // 修改默认位置为中国中部
}
final selectedNode = controller.kr_subscribeService.allList
.firstWhereOrNull((item) => item.tag == controller.kr_cutSeletedTag.value);
if (selectedNode == null) {
return const LatLng(5.0, 105.0); // 修改默认位置为中国中部35
}
//更新当前数据为选中
// controller.kr_subscribeService.allList[index].selected = 1;
// controller.selectMarkersMap(index);
// 更新最后的地图中心点
controller.kr_lastMapCenter.value =
LatLng(selectedNode.latitude, selectedNode.longitude);
return controller.kr_lastMapCenter.value;
}
}
// 构建双层圆形标记
Widget _buildDoubleCircle(double circleW1,double circleW2,MaterialColor markerColor, int num,double fontSize) {
return Stack(
alignment: Alignment.center,
children: [
// 外圈(淡蓝色,50%透明度)
Container(
width: circleW1,
height: circleW1,
decoration: BoxDecoration(
shape: BoxShape.circle,
color:markerColor.withOpacity(0.2), // 淡蓝色,50%透明度
),
),
// 内圈(蓝色,0%透明度)
Container(
width: circleW2,
height: circleW2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: markerColor, // 蓝色,0%透明度(完全不透明)
),
),
// 中心数字文本
Text(
(num==-1)?' ':'$num',
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: Colors.white, // 白色文字
),
),
],
);
}
class RadarMarker extends StatefulWidget {
final LatLng point;
final double radarSize1;
final double radarSize2;
final double radarSize3;
final Color innerColor;
final Color outerColor;
final Duration animationDuration;
final VoidCallback? onTap;
const RadarMarker({
super.key,
required this.point,
this.radarSize1 = 120,
this.radarSize2 = 60,
this.radarSize3 = 40,
this.innerColor = Colors.blue,
this.outerColor = Colors.blue,
//this.animationDuration = const Duration(seconds: 1),
this.animationDuration = const Duration(milliseconds: 500),
this.onTap,
});
@override
State<RadarMarker> createState() => _RadarMarkerState();
}
class _RadarMarkerState extends State<RadarMarker>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.animationDuration,
vsync: this,
)..repeat();
_scaleAnimation = Tween<double>(begin: 1.0, end: 2.5).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
// 第一层外圈雷达波纹
Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: widget.radarSize1,
height: widget.radarSize1,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.outerColor.withOpacity(0.2),
border: Border.all(
color: widget.outerColor.withOpacity(0.2),
width: 2.0,
),
),
),
),
),
// 第二层波纹
Transform.scale(
scale: _scaleAnimation.value * 0.7,
child: Opacity(
opacity: _opacityAnimation.value * 0.7,
child: Container(
width: widget.radarSize2,
height: widget.radarSize2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.outerColor.withOpacity(0.3),
border: Border.all(
color: widget.outerColor.withOpacity(0.3),
width: 1.5,
),
),
),
),
),
// 中心圆形
Container(
width: widget.radarSize3,
height: widget.radarSize3,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.innerColor,
boxShadow: [
BoxShadow(
color: widget.innerColor.withOpacity(0.5),
blurRadius: 10.0,
spreadRadius: 2.0,
),
],
border: Border.all(
color: Colors.white,
width: 2.0,
),
),
),
],
);
},
),
);
}
}
extension RadarMarkerExtension on RadarMarker {
Marker toMarker() {
return Marker(
point: point,
width: radarSize1,
height: radarSize1,
child: this,
);
}
}
class AssetTileProvider extends TileProvider {
@override
ImageProvider getImage(TileCoordinates coords, TileLayer options) {
// 构建assets路径:z/x/y.png
final path = 'assets/map_tiles/${coords.z}/${coords.x}/${coords.y}.png';
return AssetImage(path);
}
}
@@ -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);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
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 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.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: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'SubscriptionCard'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
AppTranslations.kr_home.subscribe,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
),
],
),
);
}
Widget _kr_buildListContainer(
BuildContext context, {
required Widget child,
EdgeInsetsGeometry? margin,
bool addBottomPadding = true,
}) {
return Container(
margin: margin ?? EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
),
child: IntrinsicWidth(
child: child,
),
);
}
}
+12
View File
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_invite_controller.dart';
class KRInviteBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRInviteController>(
() => KRInviteController(),
);
}
}
@@ -0,0 +1,305 @@
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();
}
}
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 邀请码现在使用 AppConfig 中的固定值,等待新接口实现
Future<void> _kr_fetchUserInfo() async {
try {
kr_isLoading.value = true;
// 使用 AppConfig 中的固定邀请码
kr_referCode.value = AppConfig.kr_userReferCode;
} catch (e) {
KRCommonUtil.kr_showToast(e.toString());
} finally {
kr_isLoading.value = false;
}
}
Future<bool> kr_checkLoginStatus() async {
return KRAppRunData.getInstance().kr_isLogin.value;
}
/// 获取分享链接
String kr_getShareLink() {
if (kr_referCode.isEmpty) {
return '';
}
return '${AppConfig.getInstance().kr_invitation_link}${kr_referCode.value}';
}
/// 获取二维码内容
String kr_getQRCodeContent() {
return kr_getShareLink();
}
/// 复制文本到剪贴板
Future<void> _kr_copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text));
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.copiedToClipboard);
}
Future<void> kr_handleQRShare() async {
if (!await kr_checkLoginStatus()) {
Get.find<KRMainController>().kr_setPage(0);
return;
}
if (kr_referCode.isEmpty) {
await _kr_fetchUserInfo();
if (kr_referCode.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.getInviteCodeFailed);
return;
}
}
final qrContent = kr_getQRCodeContent();
if (qrContent.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.generateQRCodeFailed);
return;
}
// 只有在登录状态下才弹出二维码对话框
if (KRAppRunData.getInstance().kr_isLogin.value) {
Get.dialog(
Dialog(
backgroundColor: Theme.of(Get.context!).cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppTranslations.kr_invite.shareQR,
style: KrAppTextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Theme.of(Get.context!).textTheme.titleMedium?.color,
),
),
SizedBox(height: 16.w),
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Theme.of(Get.context!).dividerColor.withOpacity(0.1),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: QrImageView(
data: qrContent,
version: QrVersions.auto,
size: 200.w,
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
),
SizedBox(height: 20.w),
Container(
width: double.infinity,
height: 44.w,
child: TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
backgroundColor: Theme.of(Get.context!).primaryColor.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(22.r),
),
),
child: Text(
AppTranslations.kr_invite.close,
style: TextStyle(
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
),
],
),
),
),
);
}
}
void kr_viewRewardDetails() {
// TODO: 实现查看奖励明细功能
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
void increment() => count.value++;
/// 处理链接分享
Future<void> kr_handleLinkShare() async {
if (!await kr_checkLoginStatus()) {
Get.find<KRMainController>().kr_setPage(0);
return;
}
if (kr_referCode.isEmpty) {
await _kr_fetchUserInfo();
if (kr_referCode.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.getInviteCodeFailed);
return;
}
}
final shareLink = kr_getShareLink();
if (shareLink.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.generateShareLinkFailed);
return;
}
await _kr_copyToClipboard(shareLink);
}
/// 处理复制邀请码
Future<void> kr_handleCopyInviteCode() async {
if (!await kr_checkLoginStatus()) {
Get.find<KRMainController>().kr_setPage(0);
return;
}
if (kr_referCode.isEmpty) {
await _kr_fetchUserInfo();
if (kr_referCode.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_invite.getInviteCodeFailed);
return;
}
}
await _kr_copyToClipboard(kr_referCode.value);
}
/// 获取邀请统计信息
Future<void> _kr_fetchAffiliateCount() async {
try {
final either = await KRUserApi().kr_getAffiliateCount();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(affiliateCount) {
kr_progress.value = KRInviteProgress(
pending: kr_progress.value.pending,
processing: kr_progress.value.processing,
success: kr_progress.value.success,
expired: kr_progress.value.expired,
registers: affiliateCount.registers,
totalCommission: affiliateCount.totalCommission,
);
},
);
} catch (e) {
KRCommonUtil.kr_showToast(e.toString());
}
}
Future<void> kr_onRefresh() async {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
refreshController.finishRefresh();
return;
}
try {
await _kr_fetchUserInfo();
await _kr_fetchAffiliateCount();
} finally {
refreshController.finishRefresh();
}
}
}
+450
View File
@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../controllers/kr_invite_controller.dart';
import 'package:flutter/services.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:easy_refresh/easy_refresh.dart';
class _KRSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final double maxHeight;
final double minHeight;
_KRSliverPersistentHeaderDelegate({
required this.child,
required this.maxHeight,
required this.minHeight,
});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(_KRSliverPersistentHeaderDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}
class KRInviteView extends GetView<KRInviteController> {
const KRInviteView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).primaryColor,
body: EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
header: DeliveryHeader(
triggerOffset: 50.0,
springRebound: true,
),
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
expandedHeight: 150.w,
floating: false,
pinned: true,
stretch: true,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue,
Colors.blue.shade400,
Colors.blue.shade200,
Theme.of(context).primaryColor,
],
stops: const [0.0, 0.3, 0.7, 1.0],
),
),
),
Positioned(
bottom: -50.w,
child: KrLocalImage(
imageName: "invite_top_bg",
width: 344.w,
height: 233.w,
fit: BoxFit.contain,
imageType: ImageType.png,
),
),
Positioned(
top: MediaQuery.of(context).padding.top + 16.w,
left: 16.w,
child: Text(
AppTranslations.kr_invite.title,
style: KrAppTextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
SliverPersistentHeader(
delegate: _KRSliverPersistentHeaderDelegate(
maxHeight: 120.w,
minHeight: 120.w,
child: Container(
color: Theme.of(context).primaryColor,
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: _kr_buildProgressCard(context),
),
),
pinned: true,
),
SliverToBoxAdapter(
child: SingleChildScrollView(
child: Container(
color: Theme.of(context).primaryColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_kr_buildInviteSteps(context),
_kr_buildShareButtons(context),
_kr_buildInviteRules(context),
SizedBox(height: 20.w),
],
),
),
),
),
],
),
),
);
}
Widget _kr_buildProgressCard(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8.w,
offset: Offset(0, 2.w),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_invite.inviteStats,
style: KrAppTextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.w),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Obx(() => Text(
controller.kr_progress.value.registers.toString(),
style: KrAppTextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.blue,
),
)),
),
],
),
SizedBox(height: 12.w),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_invite.registers,
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 4.w),
Obx(() => Text(
controller.kr_progress.value.registers.toString(),
style: KrAppTextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
)),
],
),
),
SizedBox(width: 16.w),
Expanded(
child: Visibility(
visible: AppConfig().kr_is_daytime,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_invite.totalCommission,
style: KrAppTextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 4.w),
Text(
controller.kr_progress.value.totalCommission.toString(),
style: KrAppTextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Colors.blue,
),
),
],
),
),
),
],
),
],
),
);
}
Widget _kr_buildInviteSteps(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(16.w, 32.w, 16.w, 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_invite.steps,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
SizedBox(height: 16.w),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_kr_buildStepCard(context, Icons.person_add, AppTranslations.kr_invite.inviteFriend),
_kr_buildStepCard(context, Icons.mail, AppTranslations.kr_invite.acceptInvite),
_kr_buildStepCard(context, Icons.card_giftcard, AppTranslations.kr_invite.getReward),
],
),
],
),
);
}
Widget _kr_buildStepCard(BuildContext context, IconData icon, String text) {
return Container(
width: 100.w,
padding: EdgeInsets.all(12.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10.r,
offset: Offset(0, 2.w),
),
],
),
child: Column(
children: [
Container(
width: 48.r,
height: 48.r,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.blue, size: 24.r),
),
SizedBox(height: 8.w),
Text(
text,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
Widget _kr_buildShareButtons(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.w),
child: Column(
children: [
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.kr_handleLinkShare,
icon: Icon(Icons.link, size: 20.r, color: Colors.white),
label: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
AppTranslations.kr_invite.shareLink,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
),
),
),
SizedBox(width: 16.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.kr_handleQRShare,
icon: Icon(Icons.qr_code, size: 20.r, color: Colors.white),
label: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
AppTranslations.kr_invite.shareQR,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
),
),
),
],
),
SizedBox(height: 16.w),
Obx(() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${AppTranslations.kr_invite.myInviteCode}: ${controller.kr_referCode.value}',
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
IconButton(
icon: Icon(Icons.copy, size: 20.r, color: Colors.blue),
onPressed: () {
Clipboard.setData(ClipboardData(text: controller.kr_referCode.value));
KRCommonUtil.kr_showToast(
AppTranslations.kr_invite.inviteCodeCopied,
);
},
),
],
);
}),
],
),
);
}
Widget _kr_buildInviteRules(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_invite.rules,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.w),
Text(
AppTranslations.kr_invite.rule1,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 8.w),
Text(
AppTranslations.kr_invite.rule2,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_language_selector_controller.dart';
class KRLanguageSelectorBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRLanguageSelectorController>(
() => KRLanguageSelectorController(),
);
}
}
@@ -0,0 +1,42 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
class KRLanguageSelectorController extends GetxController {
// 使用 KRLanguage 枚举来加载语言
final RxList<KRLanguage> kr_languages = <KRLanguage>[].obs;
// 当前选中的语言代码
final RxString kr_selectedLanguage = ''.obs;
@override
void onInit() {
super.onInit();
kr_selectedLanguage.value = KRLanguageUtils.getCurrentLanguage().countryCode;
kr_loadLanguages();
}
// 加载语言数据
void kr_loadLanguages() {
// 将英语放在前面
final sortedLanguages = KRLanguage.values.toList()
..sort((a, b) => a == KRLanguage.en ? -1 : 1);
kr_languages.value = sortedLanguages;
}
// 选择语言
Future<void> kr_selectLanguage(KRLanguage language) async {
try {
// 先更新选中状态
kr_selectedLanguage.value = language.countryCode;
// 然后切换语言
await KRLanguageUtils.switchLanguage(language);
} catch (err) {
Get.snackbar(
'错误',
'切换语言失败: $err',
snackPosition: SnackPosition.BOTTOM,
);
}
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_language_selector_controller.dart';
class KRLanguageSelectorView extends GetView<KRLanguageSelectorController> {
const KRLanguageSelectorView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
// 非渐变色区域
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
),
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_setting.switchLanguage,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => ListView.separated(
padding: EdgeInsets.all(16.r),
itemCount: controller.kr_languages.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final language = controller.kr_languages[index];
return _kr_buildLanguageCard(language, context);
},
),
),
),
],
),
),
);
}
// 构建语言卡片
Widget _kr_buildLanguageCard(KRLanguage language, BuildContext context) {
return InkWell(
onTap: () => controller.kr_selectLanguage(language),
child: Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
// 国旗图标
CircleAvatar(
radius: 16.r,
backgroundColor: Colors.blue.withOpacity(0.1),
child: Text(
language.flagEmoji,
style: TextStyle(fontSize: 16),
),
),
SizedBox(width: 12.w),
// 语言名称
Text(
language.languageName,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const Spacer(),
// 选中标记
if (controller.kr_selectedLanguage.value == language.countryCode)
Icon(
Icons.check_circle,
color: Colors.blue,
size: 20.r,
),
],
),
),
);
}
}
+12
View File
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_login_controller.dart';
class MrLoginBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRLoginController>(
() => KRLoginController(),
);
}
}
@@ -0,0 +1,636 @@
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 'package:kaer_with_panels/app/utils/kr_event_bus.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.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.passwordLogin; // 显示"登录"
case KRLoginProgressStatus.kr_loginByCode:
return AppTranslations.kr_login.passwordLogin; // 已废弃,保留以防兼容性问题
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(() {
// 始终保持邮箱类型
kr_loginType.value = KRLoginType.kr_email;
// 更新邮箱建议列表
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;
}
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
// 直接调用登录
kr_login();
}
/// 发送验证码(仅支持邮箱)
void kr_sendCode() async {
final either = await KRAuthApi().kr_sendCode(
accountController.text,
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 2 // 注册验证码类型为2
: 3); // 重置密码验证码类型为3
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
/// 开始倒计时
_startCountdown();
});
}
/// 开始登录(仅支持邮箱+密码)
void kr_login() async {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
final either = await KRAuthApi().kr_login(
accountController.text,
psdController.text);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
_saveLoginData(r);
});
}
/// 开始注册(仅支持邮箱,验证码和邀请码可选)
void kr_register() async {
// 验证邮箱
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
return;
}
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 siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
if (needVerification && codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
return;
}
final either = await KRAuthApi().kr_register(
accountController.text,
psdController.text,
code: codeController.text.isEmpty ? null : codeController.text,
inviteCode: inviteCodeController.text.isEmpty ? null : 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(
accountController.text,
codeController.text,
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 2 // 注册验证码类型为2
: 3); // 重置密码验证码类型为3
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(
accountController.text,
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,
KRLoginType.kr_email,
null);
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
// 登录/注册成功后,发送消息触发订阅服务刷新
// 延迟一小段时间确保登录状态已经完全保存
Future.delayed(Duration(milliseconds: 100), () {
KREventBus().kr_sendMessage(KRMessageType.kr_payment);
});
// 登录成功后返回到上一页
Get.back();
}
/// 根据输入内容匹配邮箱
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 onReady() {
super.onReady();
// 每次打开登录页面时,重置为登录状态(而不是注册状态)
// 这样确保点击"登录/注册"按钮时始终显示登录页面
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
}
@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();
// 始终保持邮箱类型
kr_loginType.value = KRLoginType.kr_email;
kr_emailList.value = kr_generateAndSortEmailList(input);
}
// 添加移除悬浮框的方法
void kr_removeOverlay() {
overlayEntry?.remove();
overlayEntry = null;
}
}
@@ -0,0 +1,24 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/kr_area_code.dart'; // 假设这个文件中有 KRAreaCode 类
class KRSearchAreaController extends GetxController {
final areas = <KRAreaCodeItem>[].obs;
final searchQuery = ''.obs;
@override
void onInit() {
super.onInit();
areas.assignAll(KRAreaCode.kr_getCodeList());
}
List<KRAreaCodeItem> get filteredAreas {
if (searchQuery.value.isEmpty) {
return areas;
} else {
return areas
.where((area) => area.kr_dialCode.toLowerCase().contains(searchQuery.value.toLowerCase()))
.toList();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/kr_area_code.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_search_area_controller.dart';
class KRSearchAreaView extends GetView<KRSearchAreaController> {
final Function(KRAreaCodeItem, int) onSelect;
const KRSearchAreaView({super.key, required this.onSelect});
static void show(Function(KRAreaCodeItem, int) onSelect) {
Get.dialog(
KRSearchAreaView(onSelect: onSelect),
barrierDismissible: true,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // 获取当前主题
Get.lazyPut<KRSearchAreaController>(
() => KRSearchAreaController(),
);
return GestureDetector(
onTap: () => Get.back(), // 点击背景关闭弹框
child: Scaffold(
backgroundColor: Colors.black.withOpacity(0.0),
body: Center(
child: GestureDetector(
onTap: () {}, // 阻止点击事件传递到背景
child: Container(
width: 300.w,
height: 450.w,
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: theme.primaryColor,
borderRadius: BorderRadius.circular(15.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择其他地区',
style: KrAppTextStyle(
fontSize: 15.w,
fontWeight: FontWeight.w500,
color: theme.textTheme.titleMedium?.color),
),
SizedBox(height: 10.w),
TextField(
onChanged: (value) => controller.searchQuery.value = value,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: '搜索',
hintStyle: TextStyle(color: Colors.grey),
filled: true,
// fillColor: Colors.grey.shade200,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.w),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 10.w),
),
style: theme.textTheme.bodyMedium?.copyWith(fontSize: 14.sp, fontFamily: 'AlibabaPuHuiTi-Regular',),
),
// SizedBox(height: 5.w),
Obx(() => Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: controller.filteredAreas.length,
itemBuilder: (context, index) {
final area = controller.filteredAreas[index];
return GestureDetector(
onTap: () {
onSelect(area, index); // 调用回调函数
Get.back();
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 10.w, horizontal: 0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.shade300,
width: 0.2,
),
),
),
child: Row(
children: [
Text(area.kr_icon,
style: TextStyle(fontSize: 20.w)),
SizedBox(width: 12.w),
Expanded(
child: Text(
area.kr_name,
style: KrAppTextStyle(
fontSize: 13.w,
fontWeight: FontWeight.w500),
),
),
Text(
"+" + area.kr_dialCode,
style: KrAppTextStyle(
fontSize: 13.w,
fontWeight: FontWeight.w500),
),
],
),
),
);
},
),
)),
],
),
),
),
),
),
);
}
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_invite/controllers/kr_invite_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_login/controllers/kr_login_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_statistics/controllers/kr_statistics_controller.dart';
import 'package:kaer_with_panels/app/modules/kr_user_info/controllers/kr_user_info_controller.dart';
import '../controllers/kr_main_controller.dart';
class KRMainBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRMainController>(
() => KRMainController(),
);
Get.lazyPut(() => KRHomeController());
Get.lazyPut(() => KRLoginController());
Get.lazyPut(() => KRInviteController());
Get.lazyPut(() => KRUserInfoController());
Get.lazyPut(() => KRStatisticsController());
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_home/views/kr_home_view.dart';
import 'package:kaer_with_panels/app/modules/kr_invite/views/kr_invite_view.dart';
import 'package:kaer_with_panels/app/modules/kr_statistics/views/kr_statistics_view.dart';
import 'package:kaer_with_panels/app/modules/kr_user_info/views/kr_user_info_view.dart';
import 'package:kaer_with_panels/app/modules/kr_user_info/controllers/kr_user_info_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_keep_alive_wrapper.dart';
import '../../../widgets/kr_language_switch_dialog.dart';
enum MainRoutes {
INDEX(0, '首页'),
DYNAMICS(1, '看看'),
TOTALLETTER(2, '邮筒'),
MESSAGELIST(3, '消息'),
USER_CENTER(4, '我的');
final int i;
final String title;
const MainRoutes(this.i, this.title);
}
class KRMainController extends GetxController {
static KRMainController get to => Get.find();
DateTime? lastPopTime;
var kr_currentIndex = 0.obs;
final List<Widget> widgets = [
KRKeepAliveWrapper(KRHomeView()),
KRKeepAliveWrapper(KRInviteView()),
KRKeepAliveWrapper(KRStatisticsView()),
KRKeepAliveWrapper(KRUserInfoView()),
];
/// 分页控制器
PageController pageController = PageController(keepPage: true);
@override
void onInit() {
super.onInit();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
/// 到哪个页面,具体传值查看MainRoutes的枚举类
kr_setPage(int index) {
kr_currentIndex.value = index;
pageController.jumpToPage(index);
// 监控页面进入
if (index == MainRoutes.USER_CENTER.i) {
final userInfoController = Get.find<KRUserInfoController>();
userInfoController.kr_onPageEnter();
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/modules/kr_login/views/kr_login_view.dart';
import 'package:kaer_with_panels/app/modules/kr_main/views/kr_tabbar_view.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../controllers/kr_main_controller.dart';
class KRMainView extends GetView<KRMainController> {
const KRMainView({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // 获取当前主题
return Scaffold(
// 根据下标显示哪个页面
body: GetBuilder<KRMainController>(builder: (mc) {
return PageView(
children: mc.widgets,
controller: controller.pageController,
physics: const NeverScrollableScrollPhysics(),
);
}),
bottomNavigationBar: Obx(
() => KRCustomBottomNavBar(
backgroundColor: theme.scaffoldBackgroundColor,
currentIndex: controller.kr_currentIndex.value,
onTap: (i) => controller.kr_setPage(i),
items: [
KRCustomBottomNavBarItem(
imageName: "tab_home_n",
activeImageName: "tab_home_s",
),
KRCustomBottomNavBarItem(
imageName: "tab_invite_n",
activeImageName: "tab_invite_s",
),
KRCustomBottomNavBarItem(
imageName: "tab_statistics_n",
activeImageName: "tab_statistics_s",
),
KRCustomBottomNavBarItem(
imageName: "tab_my_n",
activeImageName: "tab_my_s",
),
],
),
),
);
}
}
+109
View File
@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
/// 用来描述底部导航栏的每一项
class KRCustomBottomNavBarItem {
/// 未选中时的图片名称
final String imageName;
/// 选中时的图片名称(可选,不传则用同一张 imageName)
final String? activeImageName;
/// 标签(可选)
final String? label;
KRCustomBottomNavBarItem({
required this.imageName,
this.activeImageName,
this.label,
});
}
class KRCustomBottomNavBar extends StatelessWidget {
/// 传入当前选中的索引
final int currentIndex;
/// 导航栏的各个条目信息
final List<KRCustomBottomNavBarItem> items;
/// 点击某项时的回调
final ValueChanged<int> onTap;
/// 背景色
final Color backgroundColor;
/// 选中时 图标/文字 颜色
final Color selectedColor;
/// 未选中时 图标/文字 颜色
final Color unselectedColor;
/// 导航栏高度(不含安全区额外高度)
final double height;
/// 是否在内部自动使用 SafeArea
final bool useSafeArea;
const KRCustomBottomNavBar({
Key? key,
required this.currentIndex,
required this.items,
required this.onTap,
this.backgroundColor = Colors.white,
this.selectedColor = Colors.blue,
this.unselectedColor = Colors.grey,
this.height = 56.0,
this.useSafeArea = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
assert(items.isNotEmpty, 'The items list cannot be empty.');
assert(currentIndex >= 0 && currentIndex < items.length,
'The currentIndex must be within the bounds of the items list.');
Widget child = Container(
color: backgroundColor,
height: height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(items.length, (index) {
final item = items[index];
final bool isSelected = (index == currentIndex);
final iconName = isSelected
? (item.activeImageName ?? item.imageName)
: item.imageName;
final textColor = isSelected ? selectedColor : unselectedColor;
return Expanded(
child: InkWell(
onTap: () => onTap(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
KrLocalImage(
imageName: iconName,
height: 24,
width: 24,
),
if (item.label != null) ...[
const SizedBox(height: 4),
Text(
item.label!,
style: TextStyle(color: textColor, fontSize: 12),
),
],
],
),
),
);
}),
),
);
// 使用 SafeArea 包裹,避免底部被手势栏遮挡
return useSafeArea ? SafeArea(child: child) : child;
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_message_controller.dart';
class KrMessageBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRMessageController>(
() => KRMessageController(),
);
}
}
@@ -0,0 +1,85 @@
import 'package:get/get.dart';
import 'package:easy_refresh/easy_refresh.dart';
import '../../../model/response/kr_message_list.dart';
import '../../../services/api_service/kr_api.user.dart';
import '../../../utils/kr_common_util.dart';
class KRMessageController extends GetxController {
final KRUserApi kr_userApi = KRUserApi();
// 通知列表数据
final RxList<KRMessage> kr_messages = <KRMessage>[].obs;
final RxBool kr_isLoading = false.obs;
final RxBool kr_hasMore = true.obs;
int kr_page = 1;
final int kr_size = 10;
final EasyRefreshController refreshController = EasyRefreshController();
@override
void onInit() {
super.onInit();
kr_getMessageList();
}
// 刷新列表
Future<void> kr_onRefresh() async {
kr_page = 1;
kr_hasMore.value = true;
kr_messages.clear();
await kr_getMessageList();
refreshController.finishRefresh();
}
// 加载更多
Future<void> kr_onLoadMore() async {
if (!kr_hasMore.value || kr_isLoading.value) {
refreshController.finishLoad(IndicatorResult.noMore);
return;
}
kr_page++;
await kr_getMessageList();
refreshController.finishLoad(kr_hasMore.value ? IndicatorResult.success : IndicatorResult.noMore);
}
Future<void> kr_getMessageList() async {
if (kr_isLoading.value) return;
kr_isLoading.value = true;
final either = await kr_userApi.kr_getMessageList(kr_page, kr_size);
either.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
if (kr_page > 1) kr_page--;
},
(list) {
if (list.announcements.isEmpty) {
kr_hasMore.value = false;
} else {
// 对消息进行排序,确保 pinned 为 true 的消息排在前面
final sortedMessages = List<KRMessage>.from(list.announcements)
..sort((a, b) {
// 首先按 pinned 状态排序
if (a.pinned != b.pinned) {
return a.pinned ? -1 : 1; // pinned 为 true 的排在前面
}
// 如果 pinned 状态相同,则按创建时间降序排序
return b.createdAt.compareTo(a.createdAt);
});
kr_messages.addAll(sortedMessages);
}
},
);
kr_isLoading.value = false;
}
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
}
+236
View File
@@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../model/response/kr_message_list.dart';
import '../controllers/kr_message_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
class KRMessageView extends GetView<KRMessageController> {
const KRMessageView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
// 非渐变色区域
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
),
child: Column(
children: [
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
title: Text(
AppTranslations.kr_message.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
),
Expanded(
child: Obx(
() => EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
onLoad: controller.kr_onLoadMore,
header: DeliveryHeader(
triggerOffset: 50.0,
springRebound: true,
),
footer: DeliveryFooter(
triggerOffset: 50.0,
springRebound: true,
),
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.kr_messages.length,
itemBuilder: (context, index) {
final message = controller.kr_messages[index];
return _kr_buildMessageCard(message, context);
},
),
),
),
),
],
),
),
);
}
// 构建消息卡片
Widget _kr_buildMessageCard(KRMessage message, BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图标
Container(
width: 40.r,
height: 40.r,
decoration: BoxDecoration(
color:
// message.type == KRMessageType.system
Colors.blue,
// : Colors.orange,
shape: BoxShape.circle,
),
child: Icon(
Icons.notifications_outlined,
// : Icons.card_giftcard_outlined,
color: Colors.white,
size: 24.r,
),
),
SizedBox(width: 12.w),
// 内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
// message.type == KRMessageType.system
message.title,
// : AppTranslations.kr_message.promotion,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
Text(
message.kr_formattedCreatedAt,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
SizedBox(height: 4.h),
_kr_buildMessageContent(message.content, context),
],
),
),
],
),
);
}
// 构建消息内容
Widget _kr_buildMessageContent(String content, BuildContext context) {
// 判断内容类型
final bool kr_isHtml = content.contains('<') && content.contains('>');
final bool kr_isMarkdown = content.contains('**') ||
content.contains('*') ||
content.contains('#') ||
content.contains('- ') ||
content.contains('[');
final textStyle = KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).textTheme.bodySmall?.color,
);
if (kr_isHtml) {
// 使用 flutter_html 处理 HTML 内容
return Html(
data: content,
style: {
'body': Style(
margin: Margins.all(0),
padding: HtmlPaddings.all(0),
fontSize: FontSize(12.sp),
color: Theme.of(context).textTheme.bodySmall?.color,
),
'p': Style(
margin: Margins.only(bottom: 8.h),
),
'b': Style(
fontWeight: FontWeight.bold,
),
'i': Style(
fontStyle: FontStyle.italic,
),
'a': Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
},
shrinkWrap: true,
);
} else if (kr_isMarkdown) {
// 使用 flutter_markdown 处理 Markdown 内容
return MarkdownBody(
data: content,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: 12.sp,
color: Theme.of(context).textTheme.bodySmall?.color,
),
strong: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodySmall?.color,
),
em: TextStyle(
fontSize: 12.sp,
fontStyle: FontStyle.italic,
color: Theme.of(context).textTheme.bodySmall?.color,
),
a: TextStyle(
fontSize: 12.sp,
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
);
} else {
// 普通文本
return Text(
content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: textStyle,
);
}
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_order_status_controller.dart';
/// 订单状态页面绑定
class KROrderStatusBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KROrderStatusController>(
() => KROrderStatusController(),
);
}
}
@@ -0,0 +1,181 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/api_service/kr_subscribe_api.dart';
import '../../../routes/app_pages.dart';
import '../../../model/response/kr_order_status.dart';
import '../../../utils/kr_event_bus.dart';
import '../../../utils/kr_common_util.dart';
import '../../../localization/app_translations.dart';
import '../../../utils/kr_log_util.dart';
/// 订单状态控制器
class KROrderStatusController extends GetxController {
/// API服务
final KRSubscribeApi kr_subscribeApi = KRSubscribeApi();
/// 支付是否成功
final RxBool kr_isPaymentSuccess = false.obs;
/// 是否正在加载
final RxBool kr_isLoading = true.obs;
/// 支付URL
final String kr_paymentUrl = Get.arguments['url'] as String;
/// 订单信息
final String kr_order = Get.arguments['order'];
/// 支付方式类型
final String kr_paymentType = Get.arguments['payment_type'] as String;
/// 定时器
Timer? kr_timer;
/// 订单状态常量
static const int kr_statusPending = 1; // 待支付
static const int kr_statusPaid = 2; // 已支付
static const int kr_statusClose = 3; // 已关闭
static const int kr_statusFailed = 4; // 支付失败
static const int kr_statusFinished = 5; // 已完成
/// 状态标题
final RxString kr_statusTitle = AppTranslations.kr_orderStatus.initialTitle.obs;
/// 状态描述
final RxString kr_statusDescription = AppTranslations.kr_orderStatus.initialDescription.obs;
/// 状态图标名称
final RxString kr_statusIcon = 'payment_success'.obs;
@override
void onInit() {
super.onInit();
kr_startCheckingPaymentStatus();
}
@override
void onReady() {
super.onReady();
// 只有在非余额支付且有支付URL时才处理支付跳转
if (kr_paymentUrl.isNotEmpty && kr_paymentType != 'balance') {
if (Platform.isAndroid || Platform.isIOS) {
// 移动端使用 WebView
Get.toNamed(
Routes.KR_WEBVIEW,
arguments: {
'url': kr_paymentUrl,
'order': kr_order,
},
);
} else {
// 桌面端使用外部浏览器
final Uri uri = Uri.parse(kr_paymentUrl);
launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
@override
void onClose() {
kr_timer?.cancel();
super.onClose();
}
/// 开始检查支付状态
void kr_startCheckingPaymentStatus() {
// 根据支付方式类型设置不同的查询间隔
final Duration interval = kr_paymentType == 'balance'
? const Duration(seconds: 2) // 余额支付每2秒查询一次
: const Duration(seconds: 5); // 其他支付方式每5秒查询一次
kr_timer = Timer.periodic(interval, (timer) {
kr_checkPaymentStatus();
});
}
/// 检查支付状态
Future<void> kr_checkPaymentStatus() async {
try {
final result = await kr_subscribeApi.kr_orderDetail(kr_order);
result.fold(
(error) {
KRLogUtil.kr_e('检查支付状态失败: $error', tag: 'OrderStatusController');
kr_isLoading.value = false;
kr_statusTitle.value = AppTranslations.kr_orderStatus.checkFailedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.checkFailedDescription;
kr_statusIcon.value = 'payment_success';
},
(kr_orderStatus) {
KRLogUtil.kr_i('检查支付状态: ${kr_orderStatus.toJson()}', tag: 'OrderStatusController');
switch (kr_orderStatus.kr_status) {
case kr_statusPending:
// 待支付状态,继续轮询
kr_statusTitle.value = AppTranslations.kr_orderStatus.pendingTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.pendingDescription;
kr_statusIcon.value = 'payment_success';
break;
case kr_statusPaid:
// 已支付状态,继续轮询直到完成
kr_statusTitle.value = AppTranslations.kr_orderStatus.paidTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.paidDescription;
kr_statusIcon.value = 'payment_success';
break;
case kr_statusFinished:
// 订单完成
kr_isPaymentSuccess.value = true;
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.successTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.successDescription;
kr_statusIcon.value = 'payment_success';
KREventBus().kr_sendMessage(KRMessageType.kr_payment);
break;
case kr_statusClose:
// 订单已关闭
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.closedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.closedDescription;
kr_statusIcon.value = 'payment_success';
break;
case kr_statusFailed:
// 支付失败
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.failedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.failedDescription;
kr_statusIcon.value = 'payment_success';
break;
default:
// 未知状态
kr_isLoading.value = false;
kr_timer?.cancel();
kr_statusTitle.value = AppTranslations.kr_orderStatus.unknownTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.unknownDescription;
kr_statusIcon.value = 'payment_success';
break;
}
},
);
} catch (error) {
KRLogUtil.kr_e('检查支付状态失败: $error', tag: 'OrderStatusController');
kr_isLoading.value = false;
kr_statusTitle.value = AppTranslations.kr_orderStatus.checkFailedTitle;
kr_statusDescription.value = AppTranslations.kr_orderStatus.checkFailedDescription;
kr_statusIcon.value = 'payment_success';
}
}
/// 检查支付状态
Future<void> kr_checkPaymentStatusWithRetry() async {
try {
// ... 其他代码 ...
} catch (err) {
KRLogUtil.kr_e('检查支付状态失败: $err', tag: 'OrderStatusController');
}
}
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_order_status_controller.dart';
/// 订单状态视图
class KROrderStatusView extends GetView<KROrderStatusController> {
const KROrderStatusView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.r,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
onPressed: () => Get.back(),
),
centerTitle: true,
title: Text(
AppTranslations.kr_orderStatus.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15),
Color.fromRGBO(23, 151, 255, 0.05),
],
stops: [0.0, 0.28],
),
),
child: Obx(
() => SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 60.h),
// 状态图标
_buildStatusIcon(),
SizedBox(height: 32.h),
// 状态文本
_buildStatusText(),
SizedBox(height: 16.h),
// 描述文本
_buildDescriptionText(),
const Spacer(),
],
),
),
),
),
),
);
}
/// 构建状态图标
Widget _buildStatusIcon() {
return KrLocalImage(
imageName: controller.kr_statusIcon.value,
width: 160.w,
height: 160.w,
);
}
/// 构建状态文本
Widget _buildStatusText() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Text(
controller.kr_statusTitle.value,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Theme.of(Get.context!).textTheme.bodyMedium?.color,
),
),
);
}
/// 构建描述文本
Widget _buildDescriptionText() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Text(
controller.kr_statusDescription.value,
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(Get.context!).textTheme.bodySmall?.color,
).copyWith(height: 1.5),
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_purchase_membership_controller.dart';
class KRPurchaseMembershipBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRPurchaseMembershipController>(
() => KRPurchaseMembershipController(),
);
}
}
@@ -0,0 +1,600 @@
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 '../../../common/app_config.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(); // ⚠️ 后端暂未实现 /v1/app/payment/methods 接口
// 获取公开的支付方式
await _kr_fetchPublicPaymentMethods();
// 根据套餐数量决定是否显示套餐选择器
kr_showPlanSelector.value = kr_plans.length > 1;
kr_isLoading.value = false;
}
/// 初始化用户信息
Future<void> _iniUserInfo() async {
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 余额现在使用 AppConfig 中的固定值,等待新接口实现
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) {
// KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
// },
// (userInfo) async {
// _kr_balance = userInfo.balance;
// },
// );
// 使用 AppConfig 中的固定余额
_kr_balance = AppConfig.kr_userBalance;
}
/// 获取用户已订阅套餐
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);
}
}
}
},
);
}
/// 获取公开的支付方式列表
Future<void> _kr_fetchPublicPaymentMethods() async {
final either = await _kr_subscribeApi.kr_getPublicPaymentMethods();
either.fold(
(error) {
KRLogUtil.kr_e('获取公开支付方式失败: ${error.msg}', tag: 'PurchaseMembershipController');
},
(responseData) {
KRLogUtil.kr_i('✅ 获取公开支付方式成功', tag: 'PurchaseMembershipController');
try {
// 解析支付方式数据
final paymentMethodsData = KRPaymentMethods.fromJson(responseData);
kr_paymentMethods.value = paymentMethodsData.list;
KRLogUtil.kr_i('📊 支付方式数据已加载,共 ${kr_paymentMethods.length}', tag: 'PurchaseMembershipController');
// 打印支付方式信息
for (var method in kr_paymentMethods) {
KRLogUtil.kr_i(
'💳 支付方式: ${method.name} (ID: ${method.id}, Platform: ${method.platform})',
tag: 'PurchaseMembershipController'
);
}
print('═══════════════════════════════════════');
print('🔗 公开支付方式 API: /v1/public/payment/methods');
print('📊 支付方式列表:');
for (var i = 0; i < kr_paymentMethods.length; i++) {
final method = kr_paymentMethods[i];
print(' [$i] ${method.name} (platform: ${method.platform})');
}
print('═══════════════════════════════════════');
// 如果有支付方式,默认选择第一个
if (kr_paymentMethods.isNotEmpty) {
kr_selectedPaymentMethodIndex.value = 0;
}
} catch (e) {
KRLogUtil.kr_e('解析支付方式失败: $e', tag: 'PurchaseMembershipController');
}
},
);
}
/// 获取支付方式显示标题
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];
// ⚠️ 支付方式接口暂未实现,使用默认支付方式 ID (1) 和平台 'alipay'
final paymentMethodId = kr_paymentMethods.isNotEmpty && kr_selectedPaymentMethodIndex.value >= 0
? kr_paymentMethods[kr_selectedPaymentMethodIndex.value].id
: 1;
final paymentPlatform = kr_paymentMethods.isNotEmpty && kr_selectedPaymentMethodIndex.value >= 0
? kr_paymentMethods[kr_selectedPaymentMethodIndex.value].platform
: 'alipay';
// 获取选中的数量
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,
paymentMethodId,
'',
)
: await _kr_subscribeApi.kr_purchase(
selectedPlan.kr_id,
quantity,
paymentMethodId,
'',
);
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': paymentPlatform,
},
),
);
},
);
}
/// 获取套餐价格
/// discount: 0 表示无折扣(原价),其他值表示折扣百分比
double kr_getPlanPrice(KRPackageListItem plan, {int? discountIndex}) {
if (discountIndex != null &&
discountIndex >= 0 &&
discountIndex < plan.kr_discount.length) {
// 计算折扣价格
final discount = plan.kr_discount[discountIndex];
// 如果 discount 是 0,则表示原价(100%
final discountRate = discount.kr_discount == 0 ? 100.0 : discount.kr_discount.toDouble();
return (plan.kr_unitPrice / 100) *
discount.kr_quantity *
(discountRate / 100);
}
return plan.kr_unitPrice / 100;
}
/// 获取时间字符串
/// 根据 quantity 和 unit_time 转换成多语言描述
/// Day: 7天 -> "一周"30天 -> "一个月"90天 -> "一个季度"180天 -> "半年"365天 -> "一年"
/// Month: 3月 -> "一个季度"6月 -> "半年"12月 -> "一年"
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;
// 如果是 Day 单位,需要转换为对应的描述
if (plan.kr_unitTime == 'Day') {
switch (quantity) {
case 7:
return AppTranslations.kr_purchaseMembership.oneWeek;
case 30:
return AppTranslations.kr_purchaseMembership.oneMonth;
case 90:
return AppTranslations.kr_purchaseMembership.oneQuarter;
case 180:
return AppTranslations.kr_purchaseMembership.halfYear;
case 365:
return AppTranslations.kr_purchaseMembership.oneYear;
default:
return AppTranslations.kr_purchaseMembership.days(quantity);
}
} else if (plan.kr_unitTime == 'Month') {
// 月份也需要转换
switch (quantity) {
case 1:
return AppTranslations.kr_purchaseMembership.oneMonth;
case 3:
return AppTranslations.kr_purchaseMembership.oneQuarter;
case 6:
return AppTranslations.kr_purchaseMembership.halfYear;
case 12:
return AppTranslations.kr_purchaseMembership.oneYear;
default:
return AppTranslations.kr_purchaseMembership.month(quantity);
}
} else if (plan.kr_unitTime == 'Year') {
return AppTranslations.kr_purchaseMembership.year(quantity);
}
return '';
}
/// 获取折扣文本
/// discount: 0 或 100 表示原价(无折扣),其他值表示折扣百分比
String kr_getDiscountText(KRPackageListItem plan, int discountIndex) {
if (discountIndex >= 0 && discountIndex < plan.kr_discount.length) {
final discount = plan.kr_discount[discountIndex];
// 折扣值为 0 或 100 都表示原价,不需要显示折扣
if (discount.kr_discount == 0 || discount.kr_discount == 100) {
return '';
}
// 计算折扣百分比(例如:97% 显示为 -3%)
final discountPercent = 100 - discount.kr_discount;
return '-${discountPercent}%';
}
return '';
}
/// 获取套餐总选项数
int kr_getTotalOptionsCount(KRPackageListItem plan) {
// 确保折扣列表不为空
if (plan.kr_discount.isEmpty) {
return 1; // 如果没有折扣选项,至少返回1个选项
}
return plan.kr_discount.length;
}
/// 初始化选择
void kr_initializeSelection(KRPackageListItem plan) {
if (plan.kr_discount.isNotEmpty) {
// 默认选择第一个选项
kr_selectedDiscountIndex.value = 0;
} else {
// 如果没有选项,设置为 -1
kr_selectedDiscountIndex.value = -1;
}
}
/// 获取当前选中套餐的描述
String kr_getSelectedPlanDescription() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return '';
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features
.map((feature) => feature.kr_label)
.join('');
}
/// 获取当前选中套餐的特性标题列表
List<String> kr_getSelectedPlanFeatureLabels() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return [];
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features
.map((feature) => feature.kr_label)
.toList();
}
/// 获取当前选中套餐的详细信息
List<KRFeature> kr_getSelectedPlanFeatures() {
if (kr_selectedPlanIndex.value >= kr_plans.length) return [];
final plan = kr_plans[kr_selectedPlanIndex.value];
return plan.kr_description.kr_features;
}
/// 判断当前选中的套餐是否是续订
bool kr_isRenewal() {
if (kr_plans.isEmpty || _kr_alreadySubscribe.isEmpty) return false;
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
return _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
}
/// 获取当前选中套餐的订阅按钮文字
String kr_getSubscribeButtonText() {
if (kr_plans.isEmpty) return '';
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
final isRenewal = _kr_alreadySubscribe
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
return isRenewal
? AppTranslations.kr_purchaseMembership.renewNow
: AppTranslations.kr_purchaseMembership.startSubscription;
}
/// 切换描述展开状态
void kr_toggleDescriptionExpanded() {
kr_isDescriptionExpanded.value = !kr_isDescriptionExpanded.value;
}
/// 获取流量限制显示文本
String kr_getTrafficLimitText(KRPackageListItem plan) {
KRLogUtil.kr_i('原始流量值: ${plan.kr_traffic}', tag: 'TrafficLimit');
if (plan.kr_traffic == 0) {
return AppTranslations.kr_purchaseMembership.unlimitedTraffic;
}
// 将字节转换为GB
final trafficInGB = plan.kr_traffic / (1024 * 1024 * 1024);
KRLogUtil.kr_i('转换为GB后的值: $trafficInGB', tag: 'TrafficLimit');
if (trafficInGB < 1) {
return '${(trafficInGB * 1024).toStringAsFixed(0)}MB';
} else if (trafficInGB < 1024) {
return '${trafficInGB.toStringAsFixed(0)}GB';
} else {
return '${(trafficInGB / 1024).toStringAsFixed(1)}TB';
}
}
/// 获取设备限制显示文本
String kr_getDeviceLimitText(KRPackageListItem plan) {
if (plan.kr_deviceLimit == 0) {
return AppTranslations.kr_purchaseMembership.unlimitedDevices;
}
return AppTranslations.kr_purchaseMembership
.devices(plan.kr_deviceLimit.toString());
}
}
@@ -0,0 +1,799 @@
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;
// 如果是 Day 单位且 quantity 是 1(一天),则不显示
if (discountIndex != null &&
plan.kr_unitTime == 'Day' &&
plan.kr_discount[discountIndex].kr_quantity == 1) {
return SizedBox.shrink();
}
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 &&
plan.kr_discount[discountIndex].kr_discount != 0 &&
plan.kr_discount[discountIndex].kr_discount != 100) ...[
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.h
),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.08),
borderRadius: BorderRadius.circular(4.r),
border: Border.all(
color: Colors.red.withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.05),
blurRadius: 4,
offset: Offset(0, 2),
spreadRadius: 0,
),
],
),
child: Text(
controller.kr_getDiscountText(plan, discountIndex),
style: KrAppTextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Colors.red.withOpacity(0.9),
),
),
),
],
],
),
),
);
});
}
// 底部部分
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;
}
}
+493
View File
@@ -0,0 +1,493 @@
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';
import '../../../common/app_run_data.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(),
Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
if (!isLoggedIn) {
// 未登录,不显示此项
return SizedBox.shrink();
} else if (isDeviceLogin) {
// 设备登录(游客模式),显示"点击这里登录/注册"
return _kr_buildActionTile(
context,
title: "登录/注册",
trailing: "",
onTap: () => Get.toNamed(Routes.MR_LOGIN),
);
} else {
// 正常登录,显示用户邮箱
final userEmail = appRunData.kr_account.value ?? AppTranslations.kr_userInfo.myAccount;
return _kr_buildActionTile(
context,
title: userEmail,
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.bodySmall?.color,
),
),
trailing: Text(
trailing,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
onTap: onTap,
);
}
Widget _kr_buildTitleTile(
BuildContext context, {
required String title,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
);
}
Widget _kr_buildValueTile(
BuildContext context, {
required String title,
required String value,
}) {
return ListTile(
title: Text(
title,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
trailing: Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
);
}
Widget _kr_buildDivider() {
return Divider(
height: 1.h,
thickness: 0.2,
color: const Color(0xFFEEEEEE),
);
}
}
+11
View File
@@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/kr_splash_controller.dart';
class KRSplashBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRSplashController>(
() => KRSplashController(),
);
}
}
@@ -0,0 +1,292 @@
import 'package:get/get.dart';
import 'dart:io' show Platform;
import 'dart:math';
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/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/services/kr_device_info_service.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.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();
// 使用 print 确保日志一定会输出
print('═══════════════════════════════════════');
print('🎬 KRSplashController onInit 被调用了!');
print('═══════════════════════════════════════');
KRLogUtil.kr_i('═══════════════════════════════════════', tag: 'SplashController');
KRLogUtil.kr_i('🎬 启动页控制器 onInit', tag: 'SplashController');
KRLogUtil.kr_i('═══════════════════════════════════════', tag: 'SplashController');
// 立即初始化网站配置(在任何其他逻辑之前)
print('🌐 准备调用 _kr_initSiteConfig...');
// 必须等待网站配置和设备登录完成后,再进行后续初始化
_kr_initSiteConfig().then((_) {
print('🔧 网站配置完成,开始调用 _kr_initialize...');
_kr_initialize();
});
}
/// 初始化网站配置(最优先执行)
Future<void> _kr_initSiteConfig() async {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('🌐 【最优先】开始初始化网站配置...');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
KRLogUtil.kr_i('🌐 【最优先】初始化网站配置...', tag: 'SplashController');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
try {
print('📞 准备调用 KRSiteConfigService().initialize()...');
final success = await KRSiteConfigService().initialize();
print('📞 KRSiteConfigService().initialize() 返回: $success');
if (success) {
print('✅ 网站配置初始化成功');
KRLogUtil.kr_i('✅ 网站配置初始化成功', tag: 'SplashController');
// 检查是否支持设备登录
await _kr_checkAndPerformDeviceLogin();
} else {
print('⚠️ 网站配置初始化失败,将使用默认配置');
KRLogUtil.kr_w('⚠️ 网站配置初始化失败,将使用默认配置', tag: 'SplashController');
}
} catch (e) {
print('❌ 网站配置初始化异常: $e');
KRLogUtil.kr_e('❌ 网站配置初始化异常: $e', tag: 'SplashController');
}
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
}
/// 检查并执行设备登录
Future<void> _kr_checkAndPerformDeviceLogin() async {
try {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('🔍 检查是否支持设备登录...');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
final siteConfigService = KRSiteConfigService();
// 检查是否启用设备登录
final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled();
print('📱 设备登录状态: ${isDeviceLoginEnabled ? "已启用" : "未启用"}');
KRLogUtil.kr_i('📱 设备登录状态: $isDeviceLoginEnabled', tag: 'SplashController');
if (!isDeviceLoginEnabled) {
print('⚠️ 设备登录未启用,跳过自动登录');
KRLogUtil.kr_w('⚠️ 设备登录未启用', tag: 'SplashController');
return;
}
print('✅ 设备登录已启用,开始初始化设备信息...');
// 初始化设备信息服务
await KRDeviceInfoService().initialize();
print('🔐 开始执行设备登录...');
KRLogUtil.kr_i('🔐 开始执行设备登录', tag: 'SplashController');
// 执行设备登录
final authApi = KRAuthApi();
final result = await authApi.kr_deviceLogin();
result.fold(
(error) {
print('❌ 设备登录失败: ${error.msg}');
KRLogUtil.kr_e('❌ 设备登录失败: ${error.msg}', tag: 'SplashController');
},
(token) async {
print('✅ 设备登录成功!');
print('🎫 Token: ${token.substring(0, min(20, token.length))}...');
KRLogUtil.kr_i('✅ 设备登录成功', tag: 'SplashController');
// 使用 saveUserInfo 保存完整的用户信息
// 设备登录使用特殊的账号标识,登录类型设为邮箱(后续可以绑定真实账号)
final deviceId = KRDeviceInfoService().deviceId ?? 'unknown';
await KRAppRunData.getInstance().kr_saveUserInfo(
token,
'device_$deviceId', // 使用设备ID作为临时账号
KRLoginType.kr_email, // 默认登录类型
null, // 设备登录无需区号
);
print('💾 用户信息已保存(包括Token');
print('✅ 已标记为登录状态');
KRLogUtil.kr_i('✅ 设备登录流程完成', tag: 'SplashController');
},
);
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
} catch (e, stackTrace) {
print('❌ 设备登录检查异常: $e');
print('📚 堆栈跟踪: $stackTrace');
KRLogUtil.kr_e('❌ 设备登录检查异常: $e', tag: 'SplashController');
}
}
Future<void> _kr_initialize() async {
try {
KRLogUtil.kr_i('🔧 开始执行 _kr_initialize', tag: 'SplashController');
// 只在手机端检查网络权限
if (Platform.isIOS || Platform.isAndroid) {
KRLogUtil.kr_i('📱 移动平台,检查网络权限...', tag: 'SplashController');
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;
KRLogUtil.kr_e('❌ 网络权限检查失败', tag: 'SplashController');
return;
}
} else {
// 非手机端直接继续初始化
KRLogUtil.kr_i('💻 桌面平台,直接执行初始化', tag: 'SplashController');
await _kr_continueInitialization();
}
} catch (e) {
KRLogUtil.kr_e('❌ _kr_initialize 异常: $e', tag: 'SplashController');
kr_hasError.value = true;
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
}
}
Future<void> _kr_continueInitialization() async {
try {
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
KRLogUtil.kr_i('🚀 启动页主流程开始...', tag: 'SplashController');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
// 只在手机端检查网络连接
if (Platform.isIOS || Platform.isAndroid) {
KRLogUtil.kr_i('📱 检查网络连接...', tag: 'SplashController');
final bool isConnected = await KRNetworkCheck.kr_checkNetworkConnection();
if (!isConnected) {
kr_hasError.value = true;
kr_errorMessage.value = AppTranslations.kr_splash.kr_networkConnectionFailed;
KRLogUtil.kr_e('❌ 网络连接失败', tag: 'SplashController');
return;
}
KRLogUtil.kr_i('✅ 网络连接正常', tag: 'SplashController');
} else {
KRLogUtil.kr_i('💻 桌面平台,跳过网络连接检查', tag: 'SplashController');
}
// 初始化配置
KRLogUtil.kr_i('⚙️ 开始初始化应用配置...', tag: 'SplashController');
await AppConfig().initConfig(
onSuccess: () async {
// 配置初始化成功,继续后续步骤
KRLogUtil.kr_i('✅ 应用配置初始化成功,继续后续步骤', tag: 'SplashController');
await _kr_continueAfterConfig();
},
);
} catch (e) {
// 配置初始化失败,显示错误信息
KRLogUtil.kr_e('❌ 启动页初始化异常: $e', tag: 'SplashController');
kr_hasError.value = true;
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
}
}
// 配置初始化成功后的后续步骤
Future<void> _kr_continueAfterConfig() async {
try {
// 初始化SingBox
await KRSingBoxImp.instance.init();
// 检查是否已经通过设备登录设置了用户信息
// 如果已登录(设备登录已完成),则跳过 initializeUserInfo
// 如果未登录(没有设备登录或设备登录失败),则尝试从本地加载
if (!KRAppRunData.getInstance().kr_isLogin.value) {
KRLogUtil.kr_i('未检测到设备登录,尝试从本地加载用户信息', tag: 'SplashController');
await KRAppRunData.getInstance().kr_initializeUserInfo();
} else {
KRLogUtil.kr_i('设备登录已完成,跳过用户信息初始化', tag: 'SplashController');
}
// 等待一小段时间确保所有初始化完成
await Future.delayed(const Duration(milliseconds: 500));
// 验证登录状态是否已正确设置
final loginStatus = KRAppRunData.getInstance().kr_isLogin.value;
final token = KRAppRunData.getInstance().kr_token;
final hasToken = token != null && token.isNotEmpty;
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('🎯 准备进入主页');
print('📊 最终登录状态: $loginStatus');
print('🎫 Token存在: $hasToken');
if (hasToken) {
print('🎫 Token前缀: ${token.substring(0, min(20, token.length))}...');
}
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
KRLogUtil.kr_i('🎯 准备进入主页', tag: 'SplashController');
KRLogUtil.kr_i('📊 最终登录状态: $loginStatus', tag: 'SplashController');
KRLogUtil.kr_i('🎫 Token存在: $hasToken', tag: 'SplashController');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
// 直接导航到主页(无论是否登录,主页会根据登录状态显示不同内容)
Get.offAllNamed(Routes.KR_MAIN);
} catch (e) {
// 后续步骤失败,显示错误信息
KRLogUtil.kr_e('启动初始化失败: $e', tag: 'SplashController');
kr_hasError.value = true;
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
}
}
// 重试按钮点击事件
void kr_retry() {
kr_hasError.value = false;
kr_errorMessage.value = '';
_kr_initialize();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:io' show Platform;
import '../controllers/kr_splash_controller.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../../../widgets/kr_simple_loading.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
class KRSplashView extends GetView<KRSplashController> {
const KRSplashView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final bool isMobile = Platform.isIOS || Platform.isAndroid;
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Container(
decoration: isMobile ? BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
) : null,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图片区域
KrLocalImage(
imageName: "splash_illustration",
width: 218.w,
height: 194.w,
fit: BoxFit.contain,
),
SizedBox(height: 48.h),
// 标题
Text(
AppTranslations.kr_splash.appName,
style: KrAppTextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyMedium?.color,
),
),
SizedBox(height: 16.h),
// 副标题
Text(
AppTranslations.kr_splash.slogan,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.sp,
color: theme.textTheme.bodySmall?.color,
height: 1.5,
),
),
SizedBox(height: 24.h),
// 加载指示器或错误信息
Obx(() {
if (controller.kr_hasError.value) {
return Column(
children: [
Text(
controller.kr_errorMessage.value,
style: KrAppTextStyle(
fontSize: 14,
color: theme.textTheme.bodySmall?.color,
),
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: controller.kr_retry,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 32.w,
vertical: 12.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
),
child: Text(AppTranslations.kr_splash.kr_retry),
),
],
);
}
if (controller.kr_isLoading.value) {
return Column(
children: [
KRSimpleLoading(
color: Colors.blue,
size: 24.0,
),
SizedBox(height: 16.h),
Text(
AppTranslations.kr_splash.initializing,
style: KrAppTextStyle(
fontSize: 14,
color: theme.textTheme.bodySmall?.color,
),
),
],
);
}
return const SizedBox.shrink();
}),
],
),
),
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../../kr_home/controllers/kr_home_controller.dart';
import '../controllers/kr_statistics_controller.dart';
class KRStatisticsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRHomeController>(() => KRHomeController());
Get.lazyPut<KRStatisticsController>(() => KRStatisticsController());
}
}
@@ -0,0 +1,231 @@
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;
}
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 用户ID 现在从其他途径获取,此处注释掉
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) => KRCommonUtil.kr_showToast(error.msg),
// (userInfo) {
// // kr_homeController.kr_userId.value = userInfo.id.toString();
// },
// );
// 获取本周的开始和结束时间戳
final DateTime now = DateTime.now();
final DateTime weekStart = now.subtract(Duration(days: now.weekday - 1));
final DateTime weekStartDate = DateTime(weekStart.year, weekStart.month, weekStart.day);
final DateTime weekEndDate = weekStartDate.add(const Duration(days: 7));
final int startTimestamp = weekStartDate.millisecondsSinceEpoch ~/ 1000;
final int endTimestamp = weekEndDate.millisecondsSinceEpoch ~/ 1000;
// 更新时间戳
kr_startTime.value = startTimestamp;
kr_endTime.value = endTimestamp;
final either = await KRUserApi().kr_getUserOnlineTimeStatistics();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(trafficLogs) {
// 处理流量日志数据
_kr_processTrafficLogs(trafficLogs);
},
);
}
/// 处理流量日志数据
void _kr_processTrafficLogs(KRUserOnlineDurationResponse trafficLogs) {
try {
// 更新连续记录数据
kr_currentStreak.value = trafficLogs.connectionRecords.currentContinuousDays;
kr_highestStreak.value = trafficLogs.connectionRecords.historyContinuousDays;
// 将小时转换为天数并向上取整
kr_longestConnection.value = (trafficLogs.connectionRecords.longestSingleConnection / 24).ceil();
// 更新每周数据
final List<double> weeklyHours = List.filled(7, 0.0);
for (final stat in trafficLogs.weeklyStats) {
if (stat.day >= 1 && stat.day <= 7) {
weeklyHours[stat.day - 1] = stat.hours;
}
}
kr_weeklyData.value = weeklyHours;
} catch (e) {
KRCommonUtil.kr_showToast('处理流量日志数据失败');
KRLogUtil.kr_e('处理流量日志数据失败: $e', tag: 'Statistics');
}
}
void _kr_updateVpnStatus(String value) {
kr_vpnStatus.value = value;
}
void _kr_updateIpAddress(String value) {
kr_ipAddress.value = value.isEmpty ? '0.0.0.0' : value;
}
void _kr_updateConnectionTime(String value) {
kr_connectTime.value = value.isEmpty ? '00:00:00' : value;
}
void _kr_updateProtocol(String value) {
kr_protocol.value = value.isEmpty ? 'UDP' : value;
}
/// 更新统计数据
void _kr_updateStatistics() {
// 不再需要本地更新统计数据,完全依赖接口返回
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
refreshController.dispose();
super.onClose();
}
/// 获取根据当前日期调整后的星期标题数组
List<String> kr_getAdjustedWeekTitles() {
final DateTime now = DateTime.now();
final int currentWeekday = now.weekday; // 1-71代表周一,7代表周日
final List<String> weekTitles = [
AppTranslations.kr_statistics.monday,
AppTranslations.kr_statistics.tuesday,
AppTranslations.kr_statistics.wednesday,
AppTranslations.kr_statistics.thursday,
AppTranslations.kr_statistics.friday,
AppTranslations.kr_statistics.saturday,
AppTranslations.kr_statistics.sunday
];
// 重新排序数组,使当前日期对应的星期显示在最后
final List<String> adjustedTitles = [
...weekTitles.sublist(currentWeekday),
...weekTitles.sublist(0, currentWeekday)
];
return adjustedTitles;
}
// 刷新数据
Future<void> kr_onRefresh() async {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
refreshController.finishRefresh();
return;
}
try {
await kr_getUserSubscribeTrafficLogs();
} finally {
refreshController.finishRefresh();
}
}
}
+389
View File
@@ -0,0 +1,389 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_statistics_controller.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:easy_refresh/easy_refresh.dart';
class KRStatisticsView extends GetView<KRStatisticsController> {
const KRStatisticsView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor ,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Align(
alignment: Alignment.centerLeft,
child: Text(
AppTranslations.kr_statistics.title,
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15), // 渐变开始颜色
Color.fromRGBO(23, 151, 255, 0.05), // 中间过渡颜色
// 非渐变色区域
],
stops: [0.0, 0.28], // 调整渐变结束位置
),
),
child: EasyRefresh(
controller: controller.refreshController,
onRefresh: controller.kr_onRefresh,
header: DeliveryHeader(
triggerOffset: 50.0,
springRebound: true,
),
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// SizedBox(height: kToolbarHeight + 20.w),
_kr_buildStatusGrid(context),
_kr_buildWeeklyChart(context),
_kr_buildConnectionRecords(context),
],
),
),
),
),
);
}
// 构建状态网格
Widget _kr_buildStatusGrid(BuildContext context) {
// 根据平台调整卡片高度比例 - 优化桌面版本高度
final double aspectRatio = GetPlatform.isDesktop ? 165 / 38 : 165 / 82;
return Container(
padding: EdgeInsets.all(16.r),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 12.h,
crossAxisSpacing: 12.w,
childAspectRatio: aspectRatio, // 使用优化后的高度比例
children: [
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.vpnStatus,
controller.kr_vpnStatus.value,
Icons.link,
isError: true,
vpnStatusColor: controller.kr_vpnStatus.value == '已连接'
? const Color(0xFF67C23A)
: controller.kr_vpnStatus.value == '连接中...'
? const Color(0xFFE6A23C)
: const Color(0xFFF56C6C),
)),
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.ipAddress,
controller.kr_ipAddress.value,
Icons.language,
)),
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.connectionTime,
controller.kr_connectTime.value,
Icons.access_time,
)),
Obx(() => _kr_buildStatusCard(
context,
AppTranslations.kr_statistics.protocol,
controller.kr_protocol.value,
Icons.description_outlined,
)),
],
),
);
}
// 构建状态卡片
Widget _kr_buildStatusCard(BuildContext context, String title, String value, IconData icon, {bool isError = false, Color? vpnStatusColor}) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: vpnStatusColor ?? (isError ? Colors.red : Colors.blue),
size: 20.w,
),
SizedBox(width: 8.w),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
title,
style: KrAppTextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
),
],
),
SizedBox(height: 8.w),
Text(
value,
style: KrAppTextStyle(
fontSize: 14,
color: vpnStatusColor ?? (isError ? Colors.red : Theme.of(context).textTheme.bodySmall?.color),
),
),
],
),
);
}
// 构建每周图表
Widget _kr_buildWeeklyChart(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.r),
padding: EdgeInsets.fromLTRB(0, 16.r, 16.r, 16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 16.r),
child: Text(
AppTranslations.kr_statistics.weeklyProtectionTime,
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 16.w),
SizedBox(
height: 200.w,
child: Obx(() => LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 5,
getTitlesWidget: (value, meta) {
return Padding(
padding: EdgeInsets.only(left: 16.w),
child: Text(
value.toInt().toString(),
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 12,
),
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
reservedSize: 30,
getTitlesWidget: (value, meta) {
final titles = controller.kr_getAdjustedWeekTitles();
int index = value.toInt();
if (index >= 0 && index < titles.length) {
return Text(
titles[index],
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodySmall?.color,
fontSize: 10,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
return const Text('');
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 6,
minY: 0,
maxY: 20,
lineBarsData: [
LineChartBarData(
spots: controller.kr_weeklyData.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
}).toList(),
isCurved: true,
color: Colors.blue,
barWidth: 2,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.withOpacity(0.2),
Colors.blue.withOpacity(0.05),
],
),
),
),
],
),
)),
),
],
),
);
}
// 构建连接记录
Widget _kr_buildConnectionRecords(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.r),
child: Column(
children: [
Row(
children: [
Expanded(
child: Obx(() => _kr_buildRecordCard(context, AppTranslations.kr_statistics.currentStreak, AppTranslations.kr_statistics.days(controller.kr_currentStreak.value))),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => _kr_buildRecordCard(context, AppTranslations.kr_statistics.highestStreak, AppTranslations.kr_statistics.days(controller.kr_highestStreak.value))),
),
],
),
SizedBox(height: 16.w),
Obx(() => _kr_buildLongestConnection(context)),
],
),
);
}
// 构建记录卡片
Widget _kr_buildRecordCard(BuildContext context, String title, String value) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
SizedBox(height: 8.w),
Text(
value,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
);
}
// 构建最长连接时间
Widget _kr_buildLongestConnection(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.access_time,
color: Colors.blue,
size: 24.w,
),
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_statistics.longestConnection,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
Text(
AppTranslations.kr_statistics.days(controller.kr_longestConnection.value),
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
],
),
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_user_info_controller.dart';
class KRUserInfoBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRUserInfoController>(
() => KRUserInfoController(),
);
}
}
@@ -0,0 +1,253 @@
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,
/// 填写工单入口
workOrder,
}
/// 网格项数据模型
/// 用于统一管理功能入口的展示数据
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.workOrder,
subtitle: "",
type: KRGridItemType.workOrder,
),
].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;
}
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
// 余额现在使用 AppConfig 中的固定值,等待新接口实现
// 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;
// },
// );
// 使用 AppConfig 中的固定余额
kr_balance.value = AppConfig.kr_userBalance.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.workOrder:
return AppTranslations.kr_userInfo.workOrder;
}
}
}
+967
View File
@@ -0,0 +1,967 @@
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 'package:kaer_with_panels/app/utils/kr_subscribe_navigation_util.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(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
// 判断显示文字和颜色
String displayText;
Color displayColor;
IconData displayIcon;
bool shouldShowLoginPrompt = false;
if (!isLoggedIn) {
// 未登录
displayText = AppTranslations.kr_userInfo.bindingTip;
displayColor = Theme.of(context).colorScheme.error;
displayIcon = Icons.info_outline;
shouldShowLoginPrompt = false;
} else if (isDeviceLogin) {
// 设备登录(游客模式)
displayText = AppTranslations.kr_userInfo.loginRegister;
displayColor = const Color(0xFF1797FF); // 使用蓝色提示可以点击
displayIcon = Icons.touch_app;
shouldShowLoginPrompt = true;
} else {
// 正常登录
displayText = "${AppTranslations.kr_userInfo.myAccount} ${appRunData.kr_account.value}";
displayColor = Theme.of(context).textTheme.bodyMedium?.color ?? Colors.black;
displayIcon = Icons.info;
shouldShowLoginPrompt = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: shouldShowLoginPrompt
? () {
// 跳转到登录页面
Get.toNamed(Routes.MR_LOGIN);
}
: null,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
decoration: shouldShowLoginPrompt
? BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.05),
borderRadius: BorderRadius.circular(8.w),
)
: null,
child: Row(
children: [
Icon(
displayIcon,
color: displayColor,
size: 16.w,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
displayText,
style: KrAppTextStyle(
color: displayColor,
fontSize: 12,
fontWeight: shouldShowLoginPrompt ? FontWeight.w600 : FontWeight.normal,
),
),
),
if (shouldShowLoginPrompt)
Icon(
Icons.arrow_forward_ios,
size: 14.w,
color: displayColor,
),
],
),
),
),
// 余额信息或游客ID
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(
appRunData.isDeviceLogin()
? Icons.person_outline
: Icons.account_balance_wallet_outlined,
color: Theme.of(context).textTheme.bodyMedium?.color,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
appRunData.isDeviceLogin()
? AppTranslations.kr_userInfo.guestId((appRunData.kr_userId.value ?? 0) + 10000)
: "${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: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'UserInfo'),
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)
: () => KRSubscribeNavigationUtil.navigateToPurchase(tag: 'UserInfo'),
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 Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
// 只有正常登录用户(非游客)才显示设备管理
final showDeviceManagement = isLoggedIn && !isDeviceLogin;
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,
),
if (showDeviceManagement)
_kr_buildShortcutContainer(
icon: "my_dns",
title: AppTranslations.kr_userInfo.deviceManagement,
onTap: () {
Get.toNamed(Routes.KR_DEVICE_MANAGEMENT);
},
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.workOrder:
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;
}
}
}

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