feat: 更新代码仓库全部修改
Build Android APK / 编译 libcore.aar (push) Has been cancelled
Build Android APK / 编译 Android APK (release) (push) Has been cancelled
Build Android APK / 创建 GitHub Release (push) Has been cancelled
Build Multi-Platform / 编译 libcore (Android) (push) Has been cancelled
Build Multi-Platform / 编译 libcore (Windows) (push) Has been cancelled
Build Multi-Platform / 编译 libcore (macOS) (push) Has been cancelled
Build Multi-Platform / 编译 libcore (Linux) (push) Has been cancelled
Build Multi-Platform / 构建 Android APK (push) Has been cancelled
Build Multi-Platform / 构建 Windows (push) Has been cancelled
Build Multi-Platform / 构建 macOS (push) Has been cancelled
Build Multi-Platform / 构建 Linux (push) Has been cancelled
Build Multi-Platform / 创建 Release (push) Has been cancelled
Build Windows / build (push) Has been cancelled
Build Android APK / 编译 libcore.aar (push) Has been cancelled
Build Android APK / 编译 Android APK (release) (push) Has been cancelled
Build Android APK / 创建 GitHub Release (push) Has been cancelled
Build Multi-Platform / 编译 libcore (Android) (push) Has been cancelled
Build Multi-Platform / 编译 libcore (Windows) (push) Has been cancelled
Build Multi-Platform / 编译 libcore (macOS) (push) Has been cancelled
Build Multi-Platform / 编译 libcore (Linux) (push) Has been cancelled
Build Multi-Platform / 构建 Android APK (push) Has been cancelled
Build Multi-Platform / 构建 Windows (push) Has been cancelled
Build Multi-Platform / 构建 macOS (push) Has been cancelled
Build Multi-Platform / 构建 Linux (push) Has been cancelled
Build Multi-Platform / 创建 Release (push) Has been cancelled
Build Windows / build (push) Has been cancelled
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/hi_help_controller.dart';
|
||||
|
||||
class HIHelpBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<HIHelpController>(
|
||||
() => HIHelpController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
|
||||
import '../../../services/api_service/kr_api.user.dart';
|
||||
import '../../../utils/kr_common_util.dart';
|
||||
|
||||
class KRMessage {
|
||||
final String title;
|
||||
final List<String> content; // content 是一个字符串列表
|
||||
|
||||
KRMessage({required this.title, required this.content});
|
||||
}
|
||||
|
||||
class HIHelpController extends GetxController {
|
||||
final KRUserApi kr_userApi = KRUserApi();
|
||||
// 通知列表数据
|
||||
final RxList<KRMessage> kr_messages = <KRMessage>[
|
||||
KRMessage(
|
||||
title: '客户端下载问题',
|
||||
content: [
|
||||
'iOS设备下载客户端,需要使用除中国大陆以外的Apple ID,[link]点击这里[/link]获取海外Apple ID注册教程。',
|
||||
'Mac OS设备下载客户端,可以直接前往Hi快VPN官网下载dmg格式文件安装。',
|
||||
'Android设备下载客户端,可以直接前往Hi快VPN官网下载apk格式文件安装。',
|
||||
'PC设备下载客户端,可以直接前往Hi快VPN官网下载exe格式文件安装。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '套餐支付问题',
|
||||
content: [
|
||||
'支付页面无法打开:尝试更换网络,或检查并断开连接其它VPN软件。',
|
||||
'支付页面无法正确显示:请退出客户端重新进入,如仍无法正确显示,请截图并联系在线客服。',
|
||||
'支付成功但套餐未正确激活:请退出客户端后五分钟重新进入,如仍未正确激活,请截图您的支付记录,与在线客服联系,核实后会帮您手动激活。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '账号问题',
|
||||
content: [
|
||||
'每部设备在安装客户端后会自动生成一个账号,购买套餐后可以正常使用,如需登陆其他设备使用,则需要绑定邮箱。更换设备前建议先绑定邮箱。',
|
||||
'注销账号后会自动清除您已经购买的套餐信息,无法找回,请谨慎对待您的账号。',
|
||||
'无法收到邮箱验证码:请检查营销邮件箱或垃圾邮件箱。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '连接相关问题',
|
||||
content: [
|
||||
'无法连接:请检查设备网络是否畅通,如仍无法连接,请退出客户端并重新启动设备。',
|
||||
'连接后速度慢,无法打开页面:请进入菜单-节点列表,手动切换节点或选择自动匹配最快网络。',
|
||||
'连接后无法打开本地页面:请进入菜单-节点列表,切换为智能模式即可绕过本地ip,但会暴露您的ip地址。选择全局模式则可以保证您始终处于隐私ip状态。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '线路相关问题',
|
||||
content: [
|
||||
'Hi快VPN所有线路均为IEPL专线,最大程度保证线路纯净与连接速度。',
|
||||
'Hi快VPN所有线路均不限流量不限带宽,请放心使用。',
|
||||
'Hi快VPN所有线路均不保存任何用户网络日志,最大程度保障用户的隐私安全。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '设备与绑定问题',
|
||||
content: [
|
||||
'在绑定新设备之前请先添加您的邮箱。',
|
||||
'Hi快VPN支持最多两台设备同时在线,包括一台iOS设备或Android,加一台Mac或PC。',
|
||||
'如超出设备数量限制,最早登陆的同类设备将会自动下线,需要重新登陆,设备限制为系统固定设定,不支持调整。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '邀请用户相关问题',
|
||||
content: [
|
||||
'邀请用户请进入菜单-邀请好友页面,点击“我的邀请码”分享链接,直接分享至任意平台。受邀好友在根据流程安装好客户端后,进入菜单-邀请好友页面,贴入邀请码,保存后购买任一套餐,您和好友将会同时获得3天免费使用时长。',
|
||||
'请务必先行让受邀好友填写邀请码后再购买套餐,否则将无法激活免费时长。',
|
||||
],
|
||||
),
|
||||
KRMessage(
|
||||
title: '闪连功能相关问题',
|
||||
content: [
|
||||
'闪连功能可以为您提供最便捷的VPN使用体验,在设置好您的使用偏好并开启闪连功能后,未来只需要点击打开客户端,软件即自动连接,无需再点击任意按钮。',
|
||||
'在后台关闭客户端后,连接自动断开。如未在后台关闭,则连接不会断开。如长时间设备锁屏并没有任何操作,连接会自动断开。',
|
||||
],
|
||||
),
|
||||
].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) {
|
||||
|
||||
},
|
||||
);
|
||||
|
||||
kr_isLoading.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
refreshController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
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/hi_help_controller.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/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
|
||||
class HIHelpView extends GetView<HIHelpController> {
|
||||
const HIHelpView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
return HIBaseScaffold(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => EasyRefresh(
|
||||
// controller: controller.refreshController,
|
||||
// onRefresh: controller.kr_onRefresh,
|
||||
// onLoad: controller.kr_onLoadMore,
|
||||
// header: const DeliveryHeader(
|
||||
// triggerOffset: 50.0,
|
||||
// springRebound: true,
|
||||
// ),
|
||||
// footer: const DeliveryFooter(
|
||||
// triggerOffset: 50.0,
|
||||
// springRebound: true,
|
||||
// ),
|
||||
onRefresh: null, // 设置为 null 来禁用下拉刷新
|
||||
onLoad: null, // 设置为 null 来禁用上拉加载
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w), // 滚动条与内容间距
|
||||
child: HiFixedScrollbar(
|
||||
controller: scrollController,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
itemCount: controller.kr_messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = controller.kr_messages[index];
|
||||
final collapsibleItemData = HICollapsibleItem(
|
||||
title: message.title,
|
||||
content: message.content,
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: HICollapsibleItemWidget(item: collapsibleItemData),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 60.w),
|
||||
// 增加在线客服入口
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: MenuListItem(
|
||||
// 2. 创建一个 MenuItem 数据对象
|
||||
item: MenuItem(
|
||||
iconName: 'icon-5',
|
||||
title: '在线客服',
|
||||
// 3. 使用 onTap 回调来处理跳转逻辑
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.KR_CRISP);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30.w)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/hi_menu_controller.dart';
|
||||
|
||||
class HIMenuBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<HIMenuController>(
|
||||
() => HIMenuController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import '../../../services/api_service/kr_api.user.dart';
|
||||
import '../../../utils/kr_common_util.dart';
|
||||
import '../../../common/app_run_data.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
|
||||
class HIMenuController extends GetxController {
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
// 版本号
|
||||
final RxString kr_version = ''.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_kr_getVersion();
|
||||
}
|
||||
|
||||
// 获取版本号
|
||||
Future<void> _kr_getVersion() async {
|
||||
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
kr_version.value = packageInfo.version;
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
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 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../controllers/hi_menu_controller.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:flutter/services.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/views/hi_subscription_corner_button.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/user_info_card.dart';
|
||||
|
||||
class HIMenuView extends GetView<HIMenuController> {
|
||||
const HIMenuView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 60.h),
|
||||
Padding(
|
||||
// 5. 为整个区块设置左右 41.w 的内边距
|
||||
padding: EdgeInsets.symmetric(horizontal: 41.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Column 包裹内容
|
||||
children: [
|
||||
Obx(() {
|
||||
final account = KRAppRunData.getInstance().kr_account.value ?? '未登录';
|
||||
return UserInfoCard(
|
||||
controller: controller,
|
||||
userId: account,
|
||||
);
|
||||
}),
|
||||
|
||||
SizedBox(height: 10.h), // 卡片和菜单列表的间距
|
||||
|
||||
// ListView.separated 现在也会继承 Padding 的约束
|
||||
ListView.separated(
|
||||
shrinkWrap: true, // 让 ListView 高度自适应内容
|
||||
physics: const NeverScrollableScrollPhysics(), // 在 Stack 中,禁用其自身的滚动
|
||||
itemCount: _menuItems.length,
|
||||
// 渲染每一项
|
||||
itemBuilder: (context, index) {
|
||||
return MenuListItem(item: _menuItems[index]);
|
||||
},
|
||||
// 设置每一项之间的间距
|
||||
separatorBuilder: (context, index) {
|
||||
return SizedBox(height: 10.h);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 版本号信息
|
||||
Obx((){
|
||||
// 1. 从 Obx 的回调函数中 return 一个 Widget
|
||||
return Positioned(
|
||||
bottom: 40.0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
'当前版本:${controller.kr_version.value}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
// 2. 建议使用更稳定的颜色,避免主题切换时出现问题
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontSize: 10.w,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const List<MenuItem> _menuItems = [
|
||||
MenuItem(
|
||||
iconName: 'icon-1',
|
||||
title: '节点列表',
|
||||
route: Routes.HI_NODE_LIST,
|
||||
),
|
||||
MenuItem(
|
||||
iconName: 'icon-2',
|
||||
title: '消息中心',
|
||||
route: Routes.KR_MESSAGE,
|
||||
),
|
||||
MenuItem(
|
||||
iconName: 'icon-3',
|
||||
title: '常见问题',
|
||||
route: Routes.HI_HELP,
|
||||
),
|
||||
MenuItem(
|
||||
iconName: 'icon-4',
|
||||
title: '邀请好友',
|
||||
route: Routes.KR_INVITE,
|
||||
),
|
||||
MenuItem(
|
||||
iconName: 'icon-5',
|
||||
title: '在线客服',
|
||||
route: Routes.KR_CRISP,
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
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';
|
||||
|
||||
// 1. 将 MenuItem 数据模型也移到这里,并设为公开
|
||||
class MenuItem {
|
||||
final String iconName;
|
||||
final String title;
|
||||
final String? route; // 路由地址
|
||||
final VoidCallback? onTap; // 或者是一个点击回调
|
||||
|
||||
const MenuItem({
|
||||
required this.iconName,
|
||||
required this.title,
|
||||
this.route,
|
||||
this.onTap,
|
||||
}) : assert(route != null || onTap != null,
|
||||
'Either route or onTap must be provided.');
|
||||
// 使用 assert 强制要求 route 和 onTap 至少提供一个
|
||||
}
|
||||
|
||||
// 2. MenuListItem 组件现在是这个文件的核心
|
||||
class MenuListItem extends StatelessWidget {
|
||||
const MenuListItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
});
|
||||
|
||||
final MenuItem item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// 优先处理 onTap 回调
|
||||
if (item.onTap != null) {
|
||||
item.onTap!();
|
||||
}
|
||||
// 如果没有 onTap,再处理 route 跳转
|
||||
else if (item.route != null) {
|
||||
Get.toNamed(item.route!);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 50,
|
||||
padding: EdgeInsets.fromLTRB(20.w, 7, 18.w, 7),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor, // 使用主题的主色作为背景
|
||||
borderRadius: BorderRadius.circular(1000.r), // 添加圆角
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: item.iconName,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.title,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
KrLocalImage(
|
||||
imageName: 'arrow-right-icon',
|
||||
imageType: ImageType.svg,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// lib/app/modules/hi_menu/widgets/user_info_card.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/controllers/hi_menu_controller.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
|
||||
/// 用户信息展示卡片 Widget
|
||||
///
|
||||
/// 显示用户头像、ID和套餐到期时间,并提供点击跳转到套餐页面的功能。
|
||||
class UserInfoCard extends StatelessWidget {
|
||||
const UserInfoCard({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final HIMenuController controller;
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// 点击后用户页面
|
||||
Get.toNamed(Routes.HI_USER_INFO);
|
||||
},
|
||||
// 让整个卡片的透明区域也能响应点击
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(7.w, 4, 18.w, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(35.5.r),
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2.0, // 👇 核心改动: 将边框宽度设置为 2.0
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 18.r,
|
||||
backgroundColor: Colors.white,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w,
|
||||
height: 16.h,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'ID: $userId',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
const KrLocalImage(
|
||||
imageName: 'arrow-right-icon',
|
||||
imageType: ImageType.svg,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/hi_node_list_controller.dart';
|
||||
|
||||
class HiNodeListBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<HINodeListController>(
|
||||
() => HINodeListController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import '../../../localization/app_translations.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
||||
|
||||
class HINodeListController extends GetxController {
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
|
||||
/// 首页服务
|
||||
final KRHomeController homeController = Get.find<KRHomeController>();
|
||||
|
||||
/// 获取连接类型字符串
|
||||
String kr_getConnectionTypeString() {
|
||||
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
||||
switch (connectionType) {
|
||||
case KRConnectionType.global:
|
||||
return AppTranslations.kr_setting.connectionTypeGlobal;
|
||||
case KRConnectionType.rule:
|
||||
return AppTranslations.kr_setting.connectionTypeRule;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新连接类型
|
||||
void kr_updateConnectionType(KRConnectionType type) {
|
||||
KRSingBoxImp.instance.kr_updateConnectionType(type);
|
||||
KRLogUtil.kr_i('连接类型已更新为: $type', tag: 'HINodeListController');
|
||||
|
||||
// 如果当前有选择的国家,触发重选检查
|
||||
if (homeController.currentSelectedCountry.isNotEmpty) {
|
||||
KRLogUtil.kr_i('连接类型更新后,检查是否需要重选节点', tag: 'HINodeListController');
|
||||
// 延迟一下让连接类型更新完成
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
homeController.checkCountryReselection(KRSingBoxImp.instance.kr_activeGroups);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据国家选择最快的节点
|
||||
void kr_selectFastestNodeByCountry(String country) {
|
||||
KRLogUtil.kr_i('开始为国家 $country 选择最快节点', tag: 'HINodeListController');
|
||||
|
||||
// 通知kr_home当前选择的国家
|
||||
homeController.setCurrentSelectedCountry(country);
|
||||
|
||||
// 查找国家分组
|
||||
final countryGroup = kr_subscribeService.countryOutboundList
|
||||
.firstWhereOrNull((group) => group.country == country);
|
||||
|
||||
if (countryGroup == null) {
|
||||
KRLogUtil.kr_w('未找到国家分组: $country', tag: 'HINodeListController');
|
||||
return;
|
||||
}
|
||||
|
||||
if (countryGroup.outboundList.isEmpty) {
|
||||
KRLogUtil.kr_w('国家 $country 的节点列表为空', tag: 'HINodeListController');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找延迟最小的有效节点
|
||||
String? fastestNodeTag;
|
||||
int minDelay = 65535;
|
||||
int validNodeCount = 0;
|
||||
|
||||
for (var node in countryGroup.outboundList) {
|
||||
final delay = node.urlTestDelay.value;
|
||||
|
||||
// 统计有效节点数量
|
||||
if (delay < 65535 && delay > 0) {
|
||||
validNodeCount++;
|
||||
|
||||
// 找到延迟更小的节点
|
||||
if (delay < minDelay) {
|
||||
minDelay = delay;
|
||||
fastestNodeTag = node.tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('国家 $country 内有 $validNodeCount 个有效节点可供切换', tag: 'HINodeListController');
|
||||
|
||||
// 选择最快的节点,如果没有有效延迟则选择第一个
|
||||
final selectedTag = fastestNodeTag ?? countryGroup.outboundList.first.tag;
|
||||
|
||||
KRLogUtil.kr_i('选择节点: $selectedTag (延迟: ${minDelay == 65535 ? "未知" : "${minDelay}ms"})',
|
||||
tag: 'HINodeListController');
|
||||
|
||||
// 调用homeController的选择节点方法
|
||||
homeController.kr_selectNode(selectedTag);
|
||||
}
|
||||
|
||||
/// 获取当前国家内的有效节点标签列表
|
||||
List<String> getValidNodesInCurrentCountry() {
|
||||
final country = homeController.currentSelectedCountry.value;
|
||||
if (country.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final countryGroup = kr_subscribeService.countryOutboundList
|
||||
.firstWhereOrNull((group) => group.country == country);
|
||||
|
||||
if (countryGroup == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return countryGroup.outboundList
|
||||
.where((node) => node.urlTestDelay.value < 65535 && node.urlTestDelay.value > 0)
|
||||
.map((node) => node.tag)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 手动触发当前国家内的重选
|
||||
void manualReselection() {
|
||||
final country = homeController.currentSelectedCountry.value;
|
||||
if (country.isEmpty) {
|
||||
KRLogUtil.kr_w('没有选择国家,无法进行重选', tag: 'HINodeListController');
|
||||
return;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('用户手动触发国家内重选', tag: 'HINodeListController');
|
||||
homeController.performCountryReselection(country);
|
||||
}
|
||||
|
||||
/// 获取当前国家内节点的延迟统计(委托给homeController)
|
||||
Map<String, dynamic> getCurrentCountryLatencyStats() {
|
||||
return homeController.getCurrentCountryNodeStats();
|
||||
}
|
||||
|
||||
/// 启用/禁用动态重选功能(委托给homeController)
|
||||
void setDynamicReselectionEnabled(bool enabled) {
|
||||
homeController.setCountryReselectionEnabled(enabled);
|
||||
}
|
||||
|
||||
/// 获取动态重选状态信息
|
||||
Map<String, dynamic> getDynamicReselectionStatus() {
|
||||
return {
|
||||
'enabled': homeController.isCountryReselectionEnabled.value,
|
||||
'currentCountry': homeController.currentSelectedCountry.value,
|
||||
'latencyThreshold': homeController.countryReselectionLatencyThreshold,
|
||||
};
|
||||
}
|
||||
}
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
// hi_node_list_view.dart
|
||||
|
||||
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/hi_node_list_controller.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/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';
|
||||
|
||||
/// 节点列表视图组件
|
||||
/// 职责:只负责根据状态渲染不同的列表,不包含任何外部容器、背景或标题。
|
||||
/// 布局控制权完全交给父组件。
|
||||
class HINodeListView extends GetView<HINodeListController> {
|
||||
const HINodeListView({super.key});
|
||||
|
||||
// 定义颜色常量
|
||||
static const Color krModernGreen = Color(0xFF4CAF50);
|
||||
static const Color krModernGreenLight = Color(0xFF81C784);
|
||||
|
||||
// 存储随机延迟值,用于UI展示
|
||||
static final Map<String, int> _fakeDelays = {};
|
||||
|
||||
/// 获取用于显示的延迟值
|
||||
int _getDisplayDelay(HINodeListController controller, KROutboundItem item) {
|
||||
if (controller.homeController.kr_isConnected.value) {
|
||||
return item.urlTestDelay.value;
|
||||
}
|
||||
if (!_fakeDelays.containsKey(item.tag)) {
|
||||
final random = Random();
|
||||
_fakeDelays[item.tag] = 30 + random.nextInt(71); // 30-100ms
|
||||
}
|
||||
return _fakeDelays[item.tag] ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. 使用 Material 作为根组件,确保 InkWell 的水波纹效果正常
|
||||
// 并设置透明背景,让父组件的背景可以透出来
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
// child: _buildSubscribeList(context)
|
||||
child: _kr_buildRegionList(context)
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建国家/地区分组列表
|
||||
Widget _kr_buildRegionList(BuildContext context) {
|
||||
return Obx(() {
|
||||
return _kr_buildListContainer(
|
||||
context,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||
children: [
|
||||
if (controller.kr_subscribeService.countryOutboundList.isEmpty)
|
||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noRegions)
|
||||
else ...[
|
||||
InkWell(
|
||||
onTap: () {
|
||||
controller.homeController.kr_selectNode('auto');
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
controller.homeController.kr_coutryText.value = 'auto';
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32.w,
|
||||
height: 22.w,
|
||||
decoration: BoxDecoration(
|
||||
// 2. 设置背景色为主题色
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
// 4. 使用 Center 来确保内部的图片水平和垂直居中
|
||||
child: Center(
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-logo",
|
||||
width: 14.w,
|
||||
height: 14.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'自动匹配最快网络', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600, // 600 加粗
|
||||
),
|
||||
),
|
||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||
Text(
|
||||
'根据网络IP自动匹配最快线路', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_coutryText.value == 'auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// 选择国家
|
||||
controller.homeController.kr_coutryText.value = country.country;
|
||||
// 自动选择这个国家下的节点延迟中最快的
|
||||
controller.kr_selectFastestNodeByCountry(country.country);
|
||||
},
|
||||
child: _kr_buildCountryListItem(context, country: country),
|
||||
);
|
||||
}).toList(),
|
||||
]
|
||||
] //
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
/// 构建默认的订阅节点列表
|
||||
Widget _buildSubscribeList(BuildContext context) {
|
||||
return Obx(() {
|
||||
|
||||
// 自动触发延迟测试(仅在未连接状态下)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!controller.homeController.kr_isConnected.value && !controller.homeController.kr_isLatency.value) {
|
||||
KRLogUtil.kr_i('🔄 节点列表显示 - 自动触发延迟测试', tag: 'HINodeListView');
|
||||
controller.homeController.kr_urlTest();
|
||||
}
|
||||
});
|
||||
return _kr_buildListContainer(
|
||||
context,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||
children: [
|
||||
if (controller.kr_subscribeService.allList.isEmpty)
|
||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
||||
else ...[
|
||||
InkWell(
|
||||
onTap: () {
|
||||
controller.homeController.kr_selectNode('auto');
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32.w,
|
||||
height: 22.w,
|
||||
decoration: BoxDecoration(
|
||||
// 2. 设置背景色为主题色
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
// 4. 使用 Center 来确保内部的图片水平和垂直居中
|
||||
child: Center(
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-logo",
|
||||
width: 14.w,
|
||||
height: 14.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'自动匹配最快网络', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600, // 600 加粗
|
||||
),
|
||||
),
|
||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||
Text(
|
||||
'根据网络IP自动匹配最快线路', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == 'auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
...controller.kr_subscribeService.allList.map((item) {
|
||||
return InkWell(
|
||||
onTap: () => _onNodeSelected(item),
|
||||
child: _kr_buildNodeListItem(context, item: item),
|
||||
);
|
||||
}).toList(),
|
||||
]
|
||||
] //
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 列表为空时的占位符
|
||||
Widget _buildEmptyListPlaceholder(BuildContext context, String text) {
|
||||
return Container(
|
||||
height: 400.w, // 这是一个示例值,你可能需要根据实际屏幕高度来计算
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
text,
|
||||
style: KrAppTextStyle(fontSize: 14, color: Theme.of(context).textTheme.bodySmall?.color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 列表的白色圆角背景容器
|
||||
Widget _kr_buildListContainer(BuildContext context, {required Widget child}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// 节点选中时的通用处理逻辑
|
||||
void _onNodeSelected(KROutboundItem item) {
|
||||
KRLogUtil.kr_i('Node selected: ${item.tag}');
|
||||
KRSingBoxImp.instance.kr_selectOutbound(item.tag);
|
||||
controller.homeController.kr_selectNode(item.tag);
|
||||
// 切换回主页的仪表盘视图
|
||||
controller.homeController.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
|
||||
/// 构建单个节点列表项的UI
|
||||
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
||||
return Container(
|
||||
key: ValueKey(item.id),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
// 2. 将颜色设置为白色,并添加 30% 的透明度
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1.0, // 边框宽度 1px
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
controller.homeController.kr_getCountryFullName(item.country),
|
||||
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.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),
|
||||
Obx(() {
|
||||
// 2. 获取用于显示的延迟值
|
||||
final int delay = _getDisplayDelay(controller, item);
|
||||
// 3. 根据延迟值显示不同内容
|
||||
// if (delay <= 0) {
|
||||
// // 如果延迟为0或负数(初始状态或测试失败),则不显示
|
||||
// return const SizedBox.shrink();
|
||||
// }
|
||||
// 4. 显示延迟值,并添加 "ms" 单位
|
||||
return Text(
|
||||
'${delay}ms',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: krModernGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Text(
|
||||
item.city,
|
||||
style: KrAppTextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodySmall?.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建国家列表项的UI
|
||||
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
|
||||
return Container(
|
||||
key: ValueKey(country),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
// 2. 将颜色设置为白色,并添加 30% 的透明度
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1.0, // 边框宽度 1px
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w, horizontal: 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
KRCountryFlag(countryCode: country.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.homeController.kr_getCountryFullName(country.country),
|
||||
style: KrAppTextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)
|
||||
: KrLocalImage(
|
||||
imageName: 'radio-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 16.w, // 适当缩小 SVG 尺寸,留出白边
|
||||
height: 16.h,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import './hi_node_list_view.dart';
|
||||
import '../controllers/hi_node_list_controller.dart';
|
||||
import 'package:kaer_with_panels/app/localization/app_translations.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
class HINodePageView extends GetView<HINodeListController> {
|
||||
const HINodePageView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 主要内容区域
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 模式切换器
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// 2. 关键:移除多余的 SizedBox,让 Expanded 正确工作
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(100.r),
|
||||
),
|
||||
child: Obx(() => Row(
|
||||
children: [
|
||||
// 3. 关键:修正按钮的点击逻辑和样式逻辑
|
||||
_buildModeButton(
|
||||
context,
|
||||
title: AppTranslations.kr_setting.connectionTypeRule,
|
||||
isSelected: KRSingBoxImp().kr_connectionType.value == KRConnectionType.rule,
|
||||
onTap: () => controller.kr_updateConnectionType(KRConnectionType.rule),
|
||||
),
|
||||
_buildModeButton(
|
||||
context,
|
||||
title: AppTranslations.kr_setting.connectionTypeGlobal,
|
||||
isSelected: KRSingBoxImp().kr_connectionType.value == KRConnectionType.global,
|
||||
onTap: () => controller.kr_updateConnectionType(KRConnectionType.global),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w, vertical: 0.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${AppTranslations.kr_setting.connectionTypeRule}:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14.sp, color: Colors.black),
|
||||
),
|
||||
TextSpan(
|
||||
text: AppTranslations.kr_setting.connectionTypeRuleRemark,
|
||||
style: TextStyle(fontSize: 14.sp, color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${AppTranslations.kr_setting.connectionTypeGlobal}:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14.sp, color: Colors.black),
|
||||
),
|
||||
TextSpan(
|
||||
text: AppTranslations.kr_setting.connectionTypeGlobalRemark,
|
||||
style: TextStyle(fontSize: 14.sp, color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: KrLocalImage(
|
||||
imageName: 'question-icon',
|
||||
imageType: ImageType.svg,
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
|
||||
// 4. 关键:将列表用 Expanded 包裹,使其自适应填充剩余空间,并为HIHelpEntrance预留空间
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 60.w, right: 60.w, bottom: 90.w), // 为HIHelpEntrance预留空间
|
||||
// 2. 在 Padding 内部使用 EasyRefresh
|
||||
child: EasyRefresh(
|
||||
// 3. 绑定 onRefresh 回调
|
||||
onRefresh: controller.kr_subscribeService.kr_refreshAll,
|
||||
// 4. 自定义 Header 样式
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '正在刷新...',
|
||||
processingText: '正在刷新...',
|
||||
processedText: '刷新成功',
|
||||
failedText: '刷新失败',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
// 5. 将 HINodeListView 作为其 child
|
||||
child: HINodeListView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 底部帮助入口
|
||||
const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeButton(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||
decoration: BoxDecoration(
|
||||
// 3. 关键:修正颜色逻辑
|
||||
color: isSelected
|
||||
? Theme.of(context).primaryColor // 选中时:主题色背景
|
||||
: Colors.transparent, // 未选中时:透明背景
|
||||
borderRadius: BorderRadius.circular(100.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected
|
||||
? Colors.black // 选中时:黑色文字
|
||||
: Theme.of(context).primaryColor, // 未选中时:主题色文字
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/hi_user_info_controller.dart';
|
||||
|
||||
class HIUserInfoBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<HIUserInfoController>(
|
||||
() => HIUserInfoController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
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/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';
|
||||
import 'package:kaer_with_panels/utils/snackbar_util.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
|
||||
class HIUserInfoController extends GetxController {
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
|
||||
// 设备列表
|
||||
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 = KRDeviceInfoService().deviceId ?? 'unknown';
|
||||
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');
|
||||
KRSnackBarUtil.show(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()
|
||||
..sort((a, b) {
|
||||
if (a['is_current'] == true) return -1;
|
||||
if (b['is_current'] == true) return 1;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement');
|
||||
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
|
||||
KRSnackBarUtil.show(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.loadDeviceListFailed);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除设备
|
||||
Future<bool> deleteDevice(String id) async {
|
||||
try {
|
||||
final device = devices.firstWhere(
|
||||
(d) => d['id'] == id,
|
||||
orElse: () => {},
|
||||
);
|
||||
if (device.isEmpty) return false;
|
||||
|
||||
final isCurrent = device['is_current'] ?? false;
|
||||
|
||||
KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement');
|
||||
|
||||
final result = await KRUserApi().kr_unbindUserDevice(id);
|
||||
|
||||
bool success = false;
|
||||
result.fold(
|
||||
(error) {
|
||||
KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement');
|
||||
KRSnackBarUtil.show(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(error.msg));
|
||||
},
|
||||
(_) async {
|
||||
KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement');
|
||||
success = true;
|
||||
|
||||
if (isCurrent) {
|
||||
// 如果删除的是本机设备,重新进行设备登录
|
||||
KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement');
|
||||
// 执行重新登录
|
||||
await _reloginWithDevice();
|
||||
} else {
|
||||
devices.removeWhere((device) => device['id'] == id);
|
||||
KRSnackBarUtil.show(AppTranslations.kr_dialog.success, AppTranslations.kr_deviceManagement.deleteSuccess);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return success;
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement');
|
||||
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
|
||||
KRSnackBarUtil.show(AppTranslations.kr_dialog.error, AppTranslations.kr_deviceManagement.deleteFailed(e.toString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 重新使用设备登录
|
||||
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;
|
||||
|
||||
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');
|
||||
KRSnackBarUtil.show(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');
|
||||
}
|
||||
|
||||
KRSnackBarUtil.show(AppTranslations.kr_dialog.success, '退出登录成功');
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
},
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement');
|
||||
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
|
||||
KRSnackBarUtil.show(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();
|
||||
}
|
||||
}
|
||||
+593
@@ -0,0 +1,593 @@
|
||||
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/hi_user_info_controller.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/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/widgets/hi_menu_list_item.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/controllers/kr_home_controller.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
|
||||
class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
const HIUserInfoView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDeviceLogin = KRAppRunData.getInstance().isDeviceLogin();
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 👇 核心改动 1: 使用 Column 分割滚动区和固定区
|
||||
Column(
|
||||
children: [
|
||||
// 👇 核心改动 2: 使用 Expanded 包裹滚动区,使其填充可用空间
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 20.w),
|
||||
// he
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18.r),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
imageType: ImageType.svg,
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 13.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() {
|
||||
final account = KRAppRunData.getInstance().kr_account.value;
|
||||
return Text(
|
||||
(account != null && account.isNotEmpty) ? account : '未绑定',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 0.9,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final userId = KRAppRunData.getInstance().kr_userId.value;
|
||||
return Text(
|
||||
'ID: ${(userId != null && userId.toString().isNotEmpty) ? userId : '—'}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.w),
|
||||
// 动态:如果已有账号,显示“修改密码”,否则显示“绑定邮箱”
|
||||
Obx(() {
|
||||
final isDeviceLogin = KRAppRunData.getInstance().isDeviceLogin();
|
||||
if (!isDeviceLogin) return SizedBox.shrink();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (isDeviceLogin) {
|
||||
Get.toNamed(
|
||||
Routes.MR_LOGIN,
|
||||
arguments: {'entry': 'bind_email'},
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(
|
||||
Routes.MR_LOGIN,
|
||||
arguments: {'entry': 'forget_psd'},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 0.w).copyWith(bottom: 10.w), // 在这里增加底部间距
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22.w),
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
isDeviceLogin ? '绑定邮箱' : '修改密码',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const KrLocalImage(
|
||||
imageName: 'arrow-right-icon',
|
||||
imageType: ImageType.svg,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 设备卡片列表
|
||||
Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
// 使用 Padding 在加载指示器上方添加间距
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 20.w),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.w),
|
||||
child: GridView.builder(
|
||||
// 1. 禁止 GridView 自身的滚动,因为它已经在 SingleChildScrollView 内部
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// 2. 让 GridView 根据内容自动调整高度
|
||||
shrinkWrap: true,
|
||||
// 3. 设置网格的配置
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, // 强制两列
|
||||
crossAxisSpacing: 10.w, // 水平间距
|
||||
mainAxisSpacing: 10.w, // 垂直间距
|
||||
childAspectRatio: 1.5, // 宽高比,需要微调以获得最佳视觉效果
|
||||
),
|
||||
// 4. itemCount 和 itemBuilder 的逻辑保持不变
|
||||
itemCount: controller.devices.length + 1,
|
||||
// 5. 构建每个网格项
|
||||
itemBuilder: (context, index) {
|
||||
if (index < controller.devices.length) {
|
||||
// 假设 _buildDeviceCard 接收的是 Map<String, dynamic>
|
||||
return _buildDeviceCard(
|
||||
context: context,
|
||||
device: controller.devices[index],
|
||||
onDelete: (id) {
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w),
|
||||
child: Text(
|
||||
'请确认是否移除此设备?',
|
||||
style: KrAppTextStyle(
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
cancelText: '确认',
|
||||
confirmText: '返回',
|
||||
onConfirm: () {
|
||||
// 关闭对话框
|
||||
},
|
||||
onCancel: () {
|
||||
controller.deleteDevice(id);
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 如果是最后一个,显示“添加设备”卡片
|
||||
return _buildAddDeviceCard();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
// 在滚动区域底部增加一些空间,避免内容紧贴按钮
|
||||
SizedBox(height: 20.w),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 👇 核心改动 3: 将固定在底部的按钮放在 Expanded 外部
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// 点击“注销账户”时,显示一个确认对话框
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w),
|
||||
child: Text(
|
||||
'注销账号后,所有此账号内的剩余套餐和账户数据将被清空,无法找回。', // 1. 修改为正确的提示信息
|
||||
textAlign: TextAlign.left, // 文本居中
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
cancelText: '注销', // 2. 修改按钮文字,使其含义清晰
|
||||
confirmText: '返回',
|
||||
// 3. onCancel 对应“确认注销”按钮的点击事件
|
||||
onCancel: () {
|
||||
// 执行页面跳转到注销账户页面
|
||||
Get.toNamed(Routes.KR_DELETE_ACCOUNT);
|
||||
},
|
||||
// 4. onConfirm 对应“返回”按钮的点击事件
|
||||
onConfirm: () {
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
border: Border.all(color: const Color(0xFFFF2ED1), width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'注销账户',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFF2ED1),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
Obx(() {
|
||||
return !(KRAppRunData.getInstance().kr_account.value != null &&
|
||||
KRAppRunData.getInstance().kr_account.value!.startsWith('9000'))
|
||||
?
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w),
|
||||
child: Text(
|
||||
'确认要退出您的账号?', // 1. 修改为正确的提示信息
|
||||
textAlign: TextAlign.center, // 文本居中
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
cancelText: '确认',
|
||||
confirmText: '返回',
|
||||
onCancel: () async {
|
||||
final currentDevice = controller.devices.firstWhere(
|
||||
(device) => device['is_current'] == true,
|
||||
);
|
||||
final deviceId = currentDevice['id'] as String;
|
||||
await controller.deleteDevice(deviceId);
|
||||
},
|
||||
onConfirm: () {
|
||||
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'退出登录',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink();
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
// 为底部按钮和 HIHelpEntrance 留出空间
|
||||
SizedBox(height: 100.h),
|
||||
],
|
||||
),
|
||||
// HIHelpEntrance 仍然在 Stack 的顶层,可以正确定位
|
||||
const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
/// 构建“当前设备信息卡”的私有方法
|
||||
Widget _buildDeviceCard({
|
||||
required BuildContext context,
|
||||
required Map<String, dynamic> device,
|
||||
required void Function(String) onDelete,
|
||||
}) {
|
||||
final identifier = device['identifier'] ?? '';
|
||||
final isCurrentDevice = device['is_current'] ?? false;
|
||||
|
||||
return Stack( // 使用 Stack 来放置删除按钮
|
||||
children: [
|
||||
// 主体内容容器
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w), // 可以适当调整圆角
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center, // 垂直居中内容
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.smartphone, color: Colors.white, size: 18.w),
|
||||
SizedBox(width: 6.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_extractDeviceModel(device['device_name']),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.sp, //
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'ID:$identifier',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 如果是当前设备,显示一个标记
|
||||
// if (device.isCurrentDevice)
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(top: 0.h),
|
||||
// child: Text(
|
||||
// '本机设备',
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).primaryColor,
|
||||
// fontSize: 12.sp,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 如果不是本机设备,则显示删除按钮
|
||||
if (!isCurrentDevice)
|
||||
Positioned(
|
||||
top: 8.w, // 调整删除按钮的位置
|
||||
right: 8.w,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final deviceId = device['id'];
|
||||
onDelete(deviceId);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(4.w),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 16.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建“添加卡”的私有方法
|
||||
Widget _buildAddDeviceCard() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print('可用设备 2222');
|
||||
print('可用设备 ${controller.kr_subscribeService.kr_currentSubscribe}');
|
||||
// 在这里处理点击“添加设备”的逻辑
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w, bottom: 16.w),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Column 高度包裹内容
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 2. 第一个 Text 组件
|
||||
Text(
|
||||
'请使用邮箱登录新设备',
|
||||
style: KrAppTextStyle(
|
||||
color: Colors.black, // 可以使用稍浅
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
Obx(() {
|
||||
final current = controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
|
||||
// 如果 current 为 null,显示默认值 0
|
||||
final deviceLimit = current?.deviceLimit ?? 0;
|
||||
|
||||
return Text(
|
||||
'每个账号最多允许同时使用$deviceLimit台设备同时在线',
|
||||
style: KrAppTextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
// 设置背景色和圆角
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(Get.context!).primaryColor,
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
),
|
||||
// 使用 Align 组件将内容整体靠左居中
|
||||
alignment: Alignment.centerLeft,
|
||||
// 使用 Padding 来实现左侧 24px 的边距
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 24.w),
|
||||
// 1. 使用 Row 来水平排列图标和文本
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Row 的宽度包裹内容
|
||||
children: [
|
||||
// 2. 添加加号图标
|
||||
Text(
|
||||
'+',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18.sp, // 调整大小以匹配之前的图标
|
||||
fontWeight: FontWeight.w900, // 设置非常粗的字重
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w), // 3. 在图标和文本之间添加一些间距
|
||||
// 4. RichText 保持不变,用于显示不同样式的文本
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: Colors.black, //
|
||||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '设备:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '可添加',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _extractDeviceModel(String deviceName) {
|
||||
// 2. 使用正则表达式匹配括号内的内容
|
||||
final RegExp regExp = RegExp(r'\((.*?)\)');
|
||||
// 3. 查找第一个匹配项
|
||||
final Match? match = regExp.firstMatch(deviceName);
|
||||
|
||||
// 4. 如果找到匹配项,返回括号内的文本;否则返回原始名称
|
||||
if (match != null && match.groupCount >= 1) {
|
||||
// group(1) 返回第一个捕获组的内容,即括号内的文本
|
||||
return match.group(1) ?? deviceName;
|
||||
} else {
|
||||
// 如果没有括号,则直接返回原始设备名称
|
||||
return deviceName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ 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 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../services/kr_device_info_service.dart';
|
||||
@@ -77,6 +78,13 @@ class KRCrispController extends GetxController {
|
||||
|
||||
if (_kr_isDisposed) return;
|
||||
|
||||
final websiteId = AppConfig.getInstance().kr_website_id;
|
||||
if (websiteId.isEmpty) {
|
||||
// 如果为空,打印错误日志并终止函数
|
||||
print('Crisp 初始化失败:website_id 未配置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化 Crisp 控制器
|
||||
crispController = CrispController(
|
||||
websiteId: AppConfig.getInstance().kr_website_id,
|
||||
|
||||
@@ -6,6 +6,9 @@ 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';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
|
||||
|
||||
/// Crisp 客服聊天视图
|
||||
class KRCrispView extends GetView<KRCrispController> {
|
||||
@@ -30,46 +33,60 @@ class KRCrispView extends GetView<KRCrispController> {
|
||||
/// 构建导航栏
|
||||
PreferredSizeWidget _kr_buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
backgroundColor: Colors.transparent,
|
||||
toolbarHeight: 0.0,
|
||||
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(),
|
||||
),
|
||||
automaticallyImplyLeading: false, // 1. 禁用自动生成的 leading
|
||||
// title: Text(
|
||||
// AppTranslations.kr_userInfo.customerService,
|
||||
// style: KrAppTextStyle(
|
||||
// color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建主体内容
|
||||
Widget _kr_buildBody(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Obx(() {
|
||||
if (controller.kr_isLoading.value) {
|
||||
return _kr_buildLoadingView(context, '正在初始化客服系统...');
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
// --- 第一层:WebView 内容 ---
|
||||
// Obx 包裹的是 CrispView 和其他状态视图
|
||||
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,
|
||||
);
|
||||
}
|
||||
if (controller.kr_isInitialized.value && controller.crispController != null) {
|
||||
return CrispView(
|
||||
crispController: controller.crispController!,
|
||||
clearCache: true,
|
||||
onSessionIdReceived: _kr_onSessionIdReceived,
|
||||
);
|
||||
}
|
||||
|
||||
return _kr_buildErrorView(context);
|
||||
}),
|
||||
return _kr_buildErrorView(context);
|
||||
}),
|
||||
|
||||
// --- 第二层:悬浮的返回按钮 ---
|
||||
Positioned(
|
||||
// 4. 使用 MediaQuery 获取状态栏高度,确保按钮不会与系统UI重叠
|
||||
top: 40.w,
|
||||
left: 20.w,
|
||||
child: GestureDetector(
|
||||
onTap: () => _kr_handleBack(),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-back-icon',
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,7 +97,7 @@ class KRCrispView extends GetView<KRCrispController> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
KRSimpleLoading(
|
||||
color: Colors.blue,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 50.0,
|
||||
),
|
||||
if (message.isNotEmpty) SizedBox(height: 16.sp),
|
||||
|
||||
@@ -5,10 +5,13 @@ 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 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
|
||||
import '../../../localization/app_translations.dart';
|
||||
|
||||
class KRDeleteAccountController extends GetxController {
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
// 验证码输入框控制器
|
||||
final TextEditingController kr_codeController = TextEditingController();
|
||||
|
||||
|
||||
@@ -2,137 +2,64 @@ 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/hi_base_scaffold.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';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.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(
|
||||
return HIBaseScaffold(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
padding: EdgeInsets.all(40.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),
|
||||
_buildUserInfoSection(),
|
||||
SizedBox(height: 12.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),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 250.h, // 2. 设置最小高度为 300 (使用 .h 适配屏幕)
|
||||
),
|
||||
child: Row(
|
||||
// 3. 使用 Column 和 MainAxisAlignment.center 使其垂直居中
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
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),
|
||||
_buildVerificationCodeField(), // 4. 在这里调用,不修改其内部
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
SizedBox(
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.requestDeleteAccount();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.w),
|
||||
border: Border.all(color: const Color(0xFFFF2ED1), width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'注销账户',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFF2ED1),
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
/*SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.requestDeleteAccount,
|
||||
@@ -151,7 +78,7 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),*/
|
||||
SizedBox(height: 20.w),
|
||||
],
|
||||
),
|
||||
@@ -160,7 +87,185 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendCodeButton(BuildContext context) {
|
||||
|
||||
Widget _buildUserInfoSection() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60.w,
|
||||
height: 60.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18.r),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
imageType: ImageType.svg,
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Obx(() {
|
||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
||||
final email = KRAppRunData.getInstance().kr_account.value ?? '';
|
||||
return Text(
|
||||
email.isNotEmpty ? email : '',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 0.9,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final _ = KRAppRunData.getInstance().kr_isLogin.value;
|
||||
final userId = (KRAppRunData.getInstance().kr_userId.value ?? '').toString();
|
||||
final deviceId = KRAppRunData.getInstance().deviceId ?? '';
|
||||
|
||||
return Text(
|
||||
'ID: ${userId.isNotEmpty ? userId : deviceId}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
String expiryText;
|
||||
|
||||
if (currentSubscribe == null) {
|
||||
expiryText = '尚未购买套餐';
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime =
|
||||
DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
expireDateTime = null;
|
||||
}
|
||||
|
||||
if (expireDateTime == null) {
|
||||
expiryText = '套餐信息无效';
|
||||
} else if (expireDateTime.isBefore(now)) {
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
expiryText = '已于 $formattedExpireDate 到期';
|
||||
} else {
|
||||
expiryText = '到期时间:${expireDateTime}';final year = expireDateTime.year;
|
||||
final month = expireDateTime.month.toString().padLeft(2, '0');
|
||||
final day = expireDateTime.day.toString().padLeft(2, '0');
|
||||
final hour = expireDateTime.hour.toString().padLeft(2, '0');
|
||||
final minute = expireDateTime.minute.toString().padLeft(2, '0');
|
||||
final second = expireDateTime.second.toString().padLeft(2, '0');
|
||||
|
||||
// 2. 拼接成最终的字符串
|
||||
final formattedDateTime = '$year/$month/$day $hour:$minute:$second';
|
||||
|
||||
expiryText = '到期时间:$formattedDateTime';
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
expiryText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建注册页面的验证码输入框(包含间距)
|
||||
Widget _buildVerificationCodeField() {
|
||||
return SizedBox(
|
||||
height: 50.w, // 固定高度
|
||||
child: TextField(
|
||||
controller: controller.kr_codeController,
|
||||
keyboardType: TextInputType.number,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '验证码',
|
||||
hintStyle: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
color: const Color(0xFFA6A6A6),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.w),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(25.w),
|
||||
borderSide: BorderSide(color: Colors.white, width: 2),
|
||||
),
|
||||
isDense: true,
|
||||
suffixIconConstraints: BoxConstraints(
|
||||
maxHeight: 50.w, // 限制最大高度
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w),
|
||||
child: Obx(() {
|
||||
|
||||
return GestureDetector(
|
||||
onTap: controller.kr_canSendCode.value ? controller.kr_sendCode : null,
|
||||
child: Container(
|
||||
width: 100.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: controller.kr_canSendCode.value
|
||||
? Theme.of(Get.context!).primaryColor
|
||||
: const Color(0xFFD5D5D5),
|
||||
borderRadius: BorderRadius.circular(100.r), // 药丸状
|
||||
),
|
||||
child: Text(
|
||||
controller.kr_canSendCode.value
|
||||
? AppTranslations.kr_login.sendCode
|
||||
: '${controller.kr_countdown}s',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: controller.kr_canSendCode.value
|
||||
? Colors.black
|
||||
: const Color(0xFF464655) ,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/*Widget _buildSendCodeButton(BuildContext context) {
|
||||
return Obx(() => GestureDetector(
|
||||
onTap: controller.kr_canSendCode.value ? controller.kr_sendCode : null,
|
||||
child: Container(
|
||||
@@ -191,5 +296,5 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,278 @@
|
||||
import 'dart:math';
|
||||
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_home/controllers/kr_home_controller.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
||||
|
||||
/// ✅ 按钮组件(带多层呼吸同心圆动画)
|
||||
class HIAnimatedConnectButton extends GetView<KRHomeController> {
|
||||
final VoidCallback? onTriggerSubscriptionAnimation;
|
||||
|
||||
const HIAnimatedConnectButton({
|
||||
Key? key,
|
||||
this.onTriggerSubscriptionAnimation,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final isConnected = controller.kr_isConnected.value;
|
||||
final delay = controller.kr_currentNodeLatency.value;
|
||||
final isShow = delay == -1 || isConnected;
|
||||
|
||||
final Color buttonColor = Theme.of(context).primaryColor;
|
||||
final double screenWidth = Get.width;
|
||||
final double screenHeight = Get.height;
|
||||
|
||||
final double buttonSize = 340;
|
||||
final double currentButtonSize = isShow ? 270 : buttonSize;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.linear,
|
||||
left: isShow ? (screenWidth - currentButtonSize) / 2 : -60,
|
||||
bottom: isShow ? screenHeight * 0.2 : -40,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
/// ✅ 呼吸同心圆背景
|
||||
if (isShow)
|
||||
Positioned.fill(
|
||||
child: OverflowBox(
|
||||
maxWidth: buttonSize * 3,
|
||||
maxHeight: buttonSize * 3,
|
||||
child: ContinuousRippleEffect(
|
||||
color: Theme.of(context).primaryColor
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (!isShow)
|
||||
Positioned(
|
||||
top: -20, // 距离顶部 10.w
|
||||
left: 115, // 距离右侧 10.w
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-slogan",
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
/// ✅ 按钮主体
|
||||
Material(
|
||||
shape: const CircleBorder(),
|
||||
color: isShow ? Colors.transparent : Theme.of(context).primaryColor,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final hasValidSubscription =
|
||||
controller.kr_subscribeService.kr_availableSubscribes.isNotEmpty;
|
||||
if (hasValidSubscription) {
|
||||
controller.kr_toggleSwitch(!controller.kr_isConnected.value);
|
||||
} else {
|
||||
HIDialog.show(
|
||||
customMessageWidget: Padding(
|
||||
padding: EdgeInsets.only(top: 16.w),
|
||||
child: Text(
|
||||
'尚未购买套餐,请前往购买页面下单后即可开始极速网络体验',
|
||||
textAlign: TextAlign.left,
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
cancelText: '取消',
|
||||
confirmText: '前往',
|
||||
onConfirm: () {
|
||||
GlobalOverlayService.instance.triggerSubscriptionAnimation();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: currentButtonSize,
|
||||
height: currentButtonSize,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (isShow)
|
||||
/// ✅ isShow = true,图片居中,文字贴近底部
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: KrLocalImage(
|
||||
imageName: "hi-home-logo",
|
||||
width: 186,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
controller.kr_connectText.value == '已连接'
|
||||
? '已连接\n点击断开'
|
||||
: controller.kr_connectText.value,
|
||||
key: ValueKey(controller.kr_connectText.value),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
/// ✅ isShow = false,恢复原本布局
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: "hi-home-logo",
|
||||
width: 104,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
controller.kr_connectText.value == '已连接'
|
||||
? '已连接\n点击断开'
|
||||
: controller.kr_connectText.value,
|
||||
key: ValueKey(controller.kr_connectText.value),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
/// ✅ 呼吸式同心圆动画(最小圆固定270,其他圆在270~479间波动,初始直径差10%)
|
||||
/// ✅ 呼吸式同心圆动画(所有圆在270~470之间运动,起点相同但周期不同)
|
||||
class ContinuousRippleEffect extends StatefulWidget {
|
||||
final Color color;
|
||||
|
||||
const ContinuousRippleEffect({
|
||||
super.key,
|
||||
this.color = Colors.greenAccent,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ContinuousRippleEffect> createState() => _ContinuousRippleEffectState();
|
||||
}
|
||||
|
||||
class _ContinuousRippleEffectState extends State<ContinuousRippleEffect>
|
||||
with TickerProviderStateMixin {
|
||||
final int _layerCount = 4;
|
||||
final List<AnimationController> _controllers = [];
|
||||
final List<Animation<double>> _animations = [];
|
||||
final List<double> _opacities = [0.4, 0.6, 0.8, 1.0];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 每层动画不同周期(呼吸错峰)
|
||||
for (int i = 0; i < _layerCount; i++) {
|
||||
// 动画周期差异:外层慢,内层快
|
||||
final duration = Duration(milliseconds: 3000 + i * 1200);
|
||||
|
||||
final controller =
|
||||
AnimationController(vsync: this, duration: duration)..repeat(reverse: true);
|
||||
|
||||
final curved = CurvedAnimation(parent: controller, curve: Curves.easeInOutSine);
|
||||
_controllers.add(controller);
|
||||
_animations.add(curved);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in _controllers) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double minDiameter = 270.0; // 最小直径
|
||||
const double maxDiameter = 470.0; // 最大直径
|
||||
const double motionRange = (maxDiameter - minDiameter) / 2; // 呼吸幅度 = 100
|
||||
const double centerDiameter = (maxDiameter + minDiameter) / 2; // 平均值 370
|
||||
|
||||
return IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: maxDiameter * 1.5,
|
||||
height: maxDiameter * 1.5,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: List.generate(_layerCount, (i) {
|
||||
final opacity = _opacities[i];
|
||||
final anim = _animations[i];
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: anim,
|
||||
builder: (context, _) {
|
||||
double diameter;
|
||||
|
||||
if (i == _layerCount - 1) {
|
||||
// ✅ 最内层固定不动(稳态核心)
|
||||
diameter = minDiameter;
|
||||
} else {
|
||||
// ✅ 所有圆都在270~470之间波动
|
||||
final value = anim.value;
|
||||
// 呼吸范围:370 ± 100 * sin(…)
|
||||
diameter = minDiameter + (maxDiameter - minDiameter) * value;
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: widget.color.withOpacity(opacity),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_purchase_membership/views/kr_purchase_membership_view.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_purchase_membership/bindings/kr_purchase_membership_binding.dart';
|
||||
import 'package:kaer_with_panels/app/modules/kr_purchase_membership/controllers/kr_purchase_membership_controller.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../controllers/kr_home_controller.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
||||
|
||||
// ======================= 1. Circular Clipper (保持不变) =======================
|
||||
class CircularClipper extends CustomClipper<Path> {
|
||||
final double fraction;
|
||||
final Offset centerOffset;
|
||||
|
||||
CircularClipper(this.fraction, this.centerOffset);
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final double maxDistance = _calculateMaxDistance(size, centerOffset);
|
||||
final double currentRadius = maxDistance * fraction;
|
||||
return Path()..addOval(Rect.fromCircle(center: centerOffset, radius: currentRadius));
|
||||
}
|
||||
|
||||
double _calculateMaxDistance(Size size, Offset center) {
|
||||
final List<Offset> corners = [
|
||||
Offset(0, 0),
|
||||
Offset(size.width, 0),
|
||||
Offset(0, size.height),
|
||||
Offset(size.width, size.height)
|
||||
];
|
||||
double maxDist = 0;
|
||||
for (var corner in corners) {
|
||||
final dist = (corner - center).distance;
|
||||
if (dist > maxDist) maxDist = dist;
|
||||
}
|
||||
return maxDist;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CircularClipper oldClipper) {
|
||||
return oldClipper.fraction != fraction || oldClipper.centerOffset != centerOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= 2. 自定义路由类 (保持不变) =======================
|
||||
class CustomCircleRoute extends PageRouteBuilder {
|
||||
final Widget child;
|
||||
final Offset startOffset;
|
||||
final Color transitionColor;
|
||||
final Duration customDuration;
|
||||
|
||||
CustomCircleRoute({
|
||||
required this.child,
|
||||
required this.startOffset,
|
||||
required this.transitionColor,
|
||||
this.customDuration = const Duration(milliseconds: 1500),
|
||||
}) : super(
|
||||
opaque: false,
|
||||
transitionDuration: customDuration,
|
||||
pageBuilder: (context, animation, secondaryAnimation) => child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
return ClipPath(
|
||||
clipper: CircularClipper(curvedAnimation.value, startOffset),
|
||||
child: Container(
|
||||
color: transitionColor,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ======================= 3. 按钮组件 (核心修改) =======================
|
||||
|
||||
// 3.1. 将 StatelessWidget 转换为 StatefulWidget
|
||||
class HISubscriptionCornerButton extends StatefulWidget {
|
||||
final double size;
|
||||
final double topOffset;
|
||||
final double rightOffset;
|
||||
final Color color;
|
||||
final Function(VoidCallback)? onSetAnimationTrigger;
|
||||
|
||||
const HISubscriptionCornerButton({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.topOffset,
|
||||
required this.rightOffset,
|
||||
required this.color,
|
||||
this.onSetAnimationTrigger,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HISubscriptionCornerButton> createState() => _HISubscriptionCornerButtonState();
|
||||
}
|
||||
|
||||
// 3.2. 创建对应的 State 类
|
||||
class _HISubscriptionCornerButtonState extends State<HISubscriptionCornerButton> {
|
||||
// 3.3. 在 State 内部创建 GlobalKey,确保其唯一性
|
||||
final GlobalKey _buttonKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print('🔥 HISubscriptionCornerButton initState 被调用');
|
||||
// 注册到全局 Overlay Service
|
||||
GlobalOverlayService.instance.registerAnimationTrigger(() {
|
||||
if (mounted) {
|
||||
print('🔥 通过全局服务触发动画');
|
||||
_startCircularTransition(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 核心函数:计算按钮圆形的中心点全局坐标
|
||||
Offset _getButtonCenterOffset(BuildContext context) {
|
||||
final RenderBox? renderBox = _buttonKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
// 使用 widget.属性 来访问 StatefulWidget 的成员
|
||||
return Offset(screenWidth - widget.rightOffset - widget.size / 2, widget.topOffset + widget.size / 2);
|
||||
}
|
||||
final Offset position = renderBox.localToGlobal(Offset.zero);
|
||||
return Offset(position.dx + widget.size / 2, position.dy + widget.size / 2);
|
||||
}
|
||||
|
||||
// 导航和过渡逻辑
|
||||
void _startCircularTransition(BuildContext context) {
|
||||
print('🔥 _startCircularTransition 开始执行');
|
||||
// 使用 GetX 提供的当前路由
|
||||
final currentRoute = Get.currentRoute;
|
||||
if(currentRoute.isEmpty) return
|
||||
print('🔥 当前路由: $currentRoute');
|
||||
|
||||
if (currentRoute == Routes.KR_PURCHASE_MEMBERSHIP) {
|
||||
print('🔥 已经在 KRPurchaseMembership 页面,无需跳转');
|
||||
return; // 已在目标页面,直接返回
|
||||
}
|
||||
// ✅ 进入购买页前,把按钮颜色改为透明
|
||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(Colors.transparent);
|
||||
|
||||
final Offset startOffset = _getButtonCenterOffset(context);
|
||||
final Color transitionColor = widget.color; // 使用 widget.color
|
||||
print('🔥 startOffset: $startOffset, transitionColor: $transitionColor');
|
||||
|
||||
// 👉 每次都删除旧的 Controller,确保重新创建
|
||||
if (Get.isRegistered<KRPurchaseMembershipController>()) {
|
||||
Get.delete<KRPurchaseMembershipController>(force: true);
|
||||
print('🔥 已删除旧的 KRPurchaseMembershipController');
|
||||
}
|
||||
|
||||
// 再重新注册
|
||||
KRPurchaseMembershipBinding().dependencies();
|
||||
print('🔥 开始导航到购买页面');
|
||||
Navigator.of(context).push(
|
||||
CustomCircleRoute(
|
||||
child: KRPurchaseMembershipView(),
|
||||
startOffset: startOffset,
|
||||
transitionColor: transitionColor,
|
||||
customDuration: const Duration(milliseconds: 1500),
|
||||
),
|
||||
).then((_) {
|
||||
// 页面返回后恢复默认主题色
|
||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => _startCircularTransition(context),
|
||||
child: Container(
|
||||
key: _buttonKey, // 绑定 GlobalKey
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.color, // 使用 widget.color
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_menu/views/hi_menu_view.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 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
// import 'package:kaer_with_panels/app/widgets/kr_subscription_corner_button.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import '../../../services/kr_subscribe_service.dart';
|
||||
import '../controllers/kr_home_controller.dart';
|
||||
import '../models/kr_home_views_status.dart';
|
||||
@@ -11,320 +15,210 @@ 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';
|
||||
import './hi_animated_connect_button.dart';
|
||||
import 'package:kaer_with_panels/app/services/global_overlay_service.dart';
|
||||
|
||||
// 定义新的绿色
|
||||
const Color krModernGreen = Color(0xFF00E52B);
|
||||
const Color krModernGreenLight = Color(0xFF66FF85);
|
||||
const Color krModernGreenDark = Color(0xFF00B322);
|
||||
|
||||
class KRHomeView extends GetView<KRHomeController> {
|
||||
class KRHomeView extends StatefulWidget {
|
||||
const KRHomeView({super.key});
|
||||
|
||||
@override
|
||||
State<KRHomeView> createState() => _KRHomeViewState();
|
||||
}
|
||||
|
||||
class _KRHomeViewState extends State<KRHomeView> {
|
||||
late KRHomeController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.find<KRHomeController>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@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),
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// ✅ 在 build 完成后操作 overlay/UI
|
||||
if (Get.isRegistered<GlobalOverlayService>()) {
|
||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||
}
|
||||
});
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
HIBaseScaffold(
|
||||
showMenuButton: true,
|
||||
topContentAreaHeight: 80,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child:
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(40.w, 8.w, 0, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Hi快VPN-网在我在,网快我快',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const KRLoginView(),
|
||||
),
|
||||
// 订阅信息
|
||||
Obx(() {
|
||||
// 1. 先获取订阅信息
|
||||
final currentSubscribe =
|
||||
controller.kr_subscribeService.kr_currentSubscribe.value;
|
||||
|
||||
// 2. 准备一个 Widget 变量,用于存放最终要显示的内容
|
||||
Widget content;
|
||||
|
||||
// --- 定义统一的文本样式,并设置行高 ---
|
||||
final textStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2, // 您可以微调这个值来达到想要的视觉效果
|
||||
);
|
||||
|
||||
// 3. 开始条件判断
|
||||
if (currentSubscribe == null) {
|
||||
// --- 情况1: 没有任何订阅信息 ---
|
||||
content = Text(
|
||||
'尚未购买套餐',
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
// --- 情况2: 有订阅信息,需要判断是否过期 ---
|
||||
final now = DateTime.now();
|
||||
DateTime? expireDateTime;
|
||||
try {
|
||||
expireDateTime = DateTime.parse(currentSubscribe.expireTime);
|
||||
} catch (e) {
|
||||
// 日期格式解析失败
|
||||
}
|
||||
|
||||
if (expireDateTime != null && expireDateTime.isBefore(now)) {
|
||||
// --- 情况2.1: 订阅已过期 ---
|
||||
final formattedExpireDate =
|
||||
'${expireDateTime.year}/${expireDateTime.month.toString().padLeft(2, '0')}/${expireDateTime.day.toString().padLeft(2, '0')}';
|
||||
// 使用换行符 \n 合并为单个 Text 组件
|
||||
content = Text(
|
||||
'您的套餐已于 $formattedExpireDate 到期\n请前往购买新套餐',
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
// --- 情况2.2: 订阅有效 ---
|
||||
final difference = expireDateTime?.difference(now);
|
||||
final remainingDaysText = (difference?.inDays ?? 0) > 0
|
||||
? '${difference!.inDays} 天'
|
||||
: '不足一天';
|
||||
// 使用换行符 \n 合并为单个 Text 组件
|
||||
content = Text(
|
||||
'套餐剩余:$remainingDaysText\n${controller.kr_isConnected.value ? '当前线路:${controller.kr_getRealConnectedNodeCountry()}' : '未连接'}',
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 返回包裹在公共 Container 中的最终内容
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.only(top: 8.h),
|
||||
child: content,
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8.h), // 添加一些垂直间距
|
||||
|
||||
// --- 自定义“闪连”Checkbox ---
|
||||
Obx(() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min, // 让 Row 的宽度包裹其内容
|
||||
children: [
|
||||
// --- 可点击的 Checkbox 区域 ---
|
||||
GestureDetector(
|
||||
// 点击方框和文字都可以切换状态
|
||||
onTap: () => controller.toggleQuickConnect(!controller.isQuickConnectEnabled.value),
|
||||
child: AbsorbPointer(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1. 自定义 Checkbox 的方框外观
|
||||
Container(
|
||||
width: 20.w,
|
||||
height: 20.w,
|
||||
decoration: BoxDecoration(
|
||||
color: controller.isQuickConnectEnabled.value
|
||||
? Theme.of(context).primaryColor
|
||||
: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).primaryColor,
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: controller.isQuickConnectEnabled.value
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 14.w,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.black,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
// 2. “闪连”标签
|
||||
Text(
|
||||
'闪连',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// --- 问号图标和点击弹窗区域 ---
|
||||
SizedBox(width: 6.w), // 文字和问号图标之间的间距
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
HIDialog.show(
|
||||
title: '*闪连功能',
|
||||
message: '开启后,每次打开软件默认自动连接,无需点击连接按钮\n在后台关闭软件后,软件将自动断开',
|
||||
);
|
||||
},
|
||||
child: KrLocalImage(
|
||||
imageName: 'question-icon', // 图片名称
|
||||
imageType: ImageType.svg, // 图片类型
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
color: Colors.white, // 让图标颜色与边框协调
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
],
|
||||
),
|
||||
),
|
||||
// 将 HIAnimatedConnectButton 移到 HIBaseScaffold 外部,避免 SafeArea 约束
|
||||
HIAnimatedConnectButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,13 +297,13 @@ return GetBuilder<KRHomeController>(
|
||||
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,
|
||||
);
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RippleAnimation extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration delay;
|
||||
final Duration duration;
|
||||
final double? maxRadius;
|
||||
final double startScale;
|
||||
|
||||
const RippleAnimation({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.delay = Duration.zero,
|
||||
this.duration = const Duration(milliseconds: 1800),
|
||||
this.maxRadius,
|
||||
this.startScale = 0.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_RippleAnimationState createState() => _RippleAnimationState();
|
||||
}
|
||||
|
||||
// 自定义曲线,让动画从 startScale 开始
|
||||
class _StartScaleCurve extends Curve {
|
||||
final double startScale;
|
||||
|
||||
_StartScaleCurve(this.startScale);
|
||||
|
||||
@override
|
||||
double transformInternal(double t) {
|
||||
// t 的范围是 [0.0, 1.0]
|
||||
// 我们将这个范围映射到 [startScale, 1.0]
|
||||
return startScale + (1.0 - startScale) * t;
|
||||
}
|
||||
}
|
||||
|
||||
class _RippleAnimationState extends State<RippleAnimation>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 核心改动:使用自定义的曲线
|
||||
_scaleAnimation = _controller.drive(
|
||||
CurveTween(curve: _StartScaleCurve(widget.startScale)),
|
||||
);
|
||||
|
||||
// 淡出动画在后半程开始
|
||||
_fadeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: const Interval(0.5, 1.0)),
|
||||
);
|
||||
|
||||
Future.delayed(widget.delay, () {
|
||||
if (mounted) {
|
||||
_controller.repeat();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: SizedBox.fromSize(
|
||||
size: widget.maxRadius != null
|
||||
? Size.square(widget.maxRadius! * 2)
|
||||
: null,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ 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';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||||
|
||||
/// 邀请进度状态
|
||||
class KRInviteProgress {
|
||||
@@ -47,23 +47,15 @@ class KRInviteController extends GetxController {
|
||||
final kr_isLoading = false.obs;
|
||||
final count = 0.obs;
|
||||
final EasyRefreshController refreshController = EasyRefreshController();
|
||||
|
||||
// 用于标记是否已经显示过对话框(避免重复弹出)
|
||||
bool _hasShownDialog = false;
|
||||
final otherInviteCodeController = TextEditingController();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
final appRunData = KRAppRunData.getInstance();
|
||||
|
||||
// 监听登录状态变化
|
||||
ever(appRunData.kr_isLogin, (value) {
|
||||
// 只有账号密码登录才获取数据
|
||||
if (value && !appRunData.isDeviceLogin()) {
|
||||
ever(KRAppRunData.getInstance().kr_isLogin, (value) {
|
||||
if (value) {
|
||||
_kr_fetchUserInfo();
|
||||
_kr_fetchAffiliateCount();
|
||||
_hasShownDialog = false; // 重置对话框标记
|
||||
} else {
|
||||
kr_progress.value = KRInviteProgress(
|
||||
pending: 0,
|
||||
@@ -76,68 +68,12 @@ class KRInviteController extends GetxController {
|
||||
kr_referCode.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 如果已经是正常登录状态,加载数据
|
||||
if (appRunData.kr_isLogin.value && !appRunData.isDeviceLogin()) {
|
||||
if (KRAppRunData.getInstance().kr_isLogin.value) {
|
||||
_kr_fetchUserInfo();
|
||||
_kr_fetchAffiliateCount();
|
||||
}
|
||||
}
|
||||
|
||||
/// 当页面进入视图时调用(从视图层调用)
|
||||
void kr_onPageEnter() {
|
||||
final appRunData = KRAppRunData.getInstance();
|
||||
final isDeviceLogin = appRunData.isDeviceLogin();
|
||||
|
||||
KRLogUtil.kr_i('📄 [InviteController] 页面进入', tag: 'InviteController');
|
||||
KRLogUtil.kr_i(' - kr_isLogin: ${appRunData.kr_isLogin.value}', tag: 'InviteController');
|
||||
KRLogUtil.kr_i(' - isDeviceLogin: $isDeviceLogin', tag: 'InviteController');
|
||||
KRLogUtil.kr_i(' - _hasShownDialog: $_hasShownDialog', tag: 'InviteController');
|
||||
|
||||
// 如果未登录或设备登录,且还没显示过对话框
|
||||
if ((!appRunData.kr_isLogin.value || isDeviceLogin) && !_hasShownDialog) {
|
||||
_hasShownDialog = true; // 标记已显示
|
||||
|
||||
KRLogUtil.kr_i('🔒 [InviteController] 未登录或设备登录,显示绑定提示对话框', tag: 'InviteController');
|
||||
|
||||
// 延迟执行,确保页面加载完成
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
KRLogUtil.kr_i('💬 [InviteController] 准备显示对话框...', tag: 'InviteController');
|
||||
try {
|
||||
KRDialog.show(
|
||||
title: AppTranslations.kr_dialog.deviceLoginBindingTitle,
|
||||
message: AppTranslations.kr_dialog.deviceLoginBindingMessage,
|
||||
confirmText: AppTranslations.kr_dialog.kr_ok,
|
||||
cancelText: AppTranslations.kr_dialog.kr_cancel,
|
||||
onConfirm: () {
|
||||
KRLogUtil.kr_i('✅ [InviteController] 用户点击确定,跳转到登录页', tag: 'InviteController');
|
||||
Get.back(); // 关闭对话框
|
||||
// 等待对话框完全关闭后再跳转到登录页面
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
// 先跳转到首页,确保登录后返回到首页而不是邀请页面
|
||||
Get.find<KRMainController>().kr_setPage(0);
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
Get.toNamed(Routes.MR_LOGIN);
|
||||
});
|
||||
});
|
||||
},
|
||||
onCancel: () {
|
||||
KRLogUtil.kr_i('❌ [InviteController] 用户点击取消,返回首页', tag: 'InviteController');
|
||||
Get.back(); // 关闭对话框
|
||||
// 等待对话框完全关闭后再跳转到首页
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
Get.find<KRMainController>().kr_setPage(0);
|
||||
});
|
||||
},
|
||||
);
|
||||
KRLogUtil.kr_i('✅ [InviteController] 对话框已显示', tag: 'InviteController');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ [InviteController] 显示对话框失败: $e', tag: 'InviteController');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口
|
||||
// 从 AppRunData 获取邀请码(登录后自动获取)
|
||||
Future<void> _kr_fetchUserInfo() async {
|
||||
@@ -171,9 +107,7 @@ class KRInviteController extends GetxController {
|
||||
}
|
||||
|
||||
Future<bool> kr_checkLoginStatus() async {
|
||||
final appRunData = KRAppRunData.getInstance();
|
||||
// 只有账号密码登录才算登录,设备登录不算
|
||||
return appRunData.kr_isLogin.value && !appRunData.isDeviceLogin();
|
||||
return KRAppRunData.getInstance().kr_isLogin.value;
|
||||
}
|
||||
|
||||
/// 获取分享链接
|
||||
@@ -351,6 +285,30 @@ class KRInviteController extends GetxController {
|
||||
await _kr_copyToClipboard(kr_referCode.value);
|
||||
}
|
||||
|
||||
/// 处理绑定邀请码
|
||||
Future<void> kr_handleBindInviteCode() async {
|
||||
final text = otherInviteCodeController.text;
|
||||
print('输入的邀请码是: $text');
|
||||
if (text.isEmpty) {
|
||||
KRCommonUtil.kr_showToast('请输入邀请码');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final either = await KRUserApi().hi_inviteCode(text);
|
||||
either.fold(
|
||||
(error) => KRCommonUtil.kr_showToast(error.msg),
|
||||
(affiliateCount) {
|
||||
KRCommonUtil.kr_showToast('绑定成功: $text');
|
||||
otherInviteCodeController.text = '';
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
KRCommonUtil.kr_showToast(e.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// 获取邀请统计信息
|
||||
Future<void> _kr_fetchAffiliateCount() async {
|
||||
try {
|
||||
@@ -374,13 +332,11 @@ class KRInviteController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> kr_onRefresh() async {
|
||||
final appRunData = KRAppRunData.getInstance();
|
||||
// 只有账号密码登录才允许刷新
|
||||
if (!appRunData.kr_isLogin.value || appRunData.isDeviceLogin()) {
|
||||
if (!KRAppRunData.getInstance().kr_isLogin.value) {
|
||||
refreshController.finishRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await _kr_fetchUserInfo();
|
||||
await _kr_fetchAffiliateCount();
|
||||
|
||||
@@ -1,468 +1,206 @@
|
||||
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/common/app_run_data.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;
|
||||
}
|
||||
}
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
|
||||
class KRInviteView extends GetView<KRInviteController> {
|
||||
const KRInviteView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
body: Obx(() {
|
||||
// 使用 Obx 响应式监听登录状态
|
||||
final appRunData = KRAppRunData.getInstance();
|
||||
final isDeviceLogin = appRunData.isDeviceLogin();
|
||||
final isLoggedIn = appRunData.kr_isLogin.value && !isDeviceLogin;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// 未登录或设备登录,显示空白页面并触发对话框
|
||||
// 使用 WidgetsBinding 确保在构建完成后调用
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.kr_onPageEnter();
|
||||
});
|
||||
return Container(); // 空白容器
|
||||
}
|
||||
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
|
||||
|
||||
// 正常登录用户显示邀请页面内容
|
||||
return 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}), // 闭合 Obx
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
return HIBaseScaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
// 1. child 的根组件使用 Stack,以允许绝对定位的组件(如 HIHelpEntrance)正常工作
|
||||
child: Stack(
|
||||
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,
|
||||
),
|
||||
SingleChildScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
// 3. 使用一个内层 Column 来组织内容
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🟢 第一行:奖励说明
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
),
|
||||
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,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min, // 由内容撑开
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
imageType: ImageType.svg,
|
||||
width: 54.w,
|
||||
color: Colors.black,
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'受邀用户首次付款时,他将与您分别获得3天免费使用时长',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Visibility(
|
||||
visible: AppConfig().kr_is_daytime,
|
||||
child: Column(
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26.w),
|
||||
// 🟢 第二行:我的邀请码
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.w),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 2.0),
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
),
|
||||
child: Obx(
|
||||
() => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: 100.w,
|
||||
height: 40.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
),
|
||||
child: Text(
|
||||
'邀请码',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.kr_referCode.value,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const KrLocalImage(
|
||||
imageName: 'share-icon',
|
||||
imageType: ImageType.svg,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
if (controller.kr_referCode.value.isNotEmpty) {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: controller.kr_referCode.value),
|
||||
);
|
||||
KRCommonUtil.kr_showToast(
|
||||
AppTranslations.kr_invite.inviteCodeCopied,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 160.w),
|
||||
// 🟢 第三部分:接受他人邀请
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppTranslations.kr_invite.totalCommission,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
'接受他人邀请',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
Text(
|
||||
controller.kr_progress.value.totalCommission.toString(),
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue,
|
||||
SizedBox(height: 8.h),
|
||||
TextField(
|
||||
controller: controller.otherInviteCodeController,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
hintText: '填入邀请人邀请码兑换免费时长...',
|
||||
hintStyle: const TextStyle(color: Color(0xFFA6A6A6)),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 22.w),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
borderSide: const BorderSide(color: Colors.white, width: 2.0),
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: 50.h),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50.w,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(1000.r),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
controller.kr_handleBindInviteCode();
|
||||
},
|
||||
child: Text(
|
||||
'保存',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 4. 在可滚动区域的末尾添加一个占位边距,防止内容被 HIHelpEntrance 遮挡
|
||||
SizedBox(height: 80.w), // 这个高度可以根据 HIHelpEntrance 的实际高度进行微调
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
],
|
||||
),
|
||||
// 5. 将 HIHelpEntrance 作为 Stack 的直接子元素,它将恢复其原有的绝对定位能力
|
||||
if (!isKeyboardVisible)
|
||||
const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _kr_buildStepCard(BuildContext context, IconData icon, String text) {
|
||||
return Container(
|
||||
width: 100.w,
|
||||
padding: EdgeInsets.all(12.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10.r,
|
||||
offset: Offset(0, 2.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48.r,
|
||||
height: 48.r,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: Colors.blue, size: 24.r),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _kr_buildShareButtons(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.w),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.kr_handleLinkShare,
|
||||
icon: Icon(Icons.link, size: 20.r, color: Colors.white),
|
||||
label: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
AppTranslations.kr_invite.shareLink,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.kr_handleQRShare,
|
||||
icon: Icon(Icons.qr_code, size: 20.r, color: Colors.white),
|
||||
label: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
AppTranslations.kr_invite.shareQR,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2196F3),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Obx(() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${AppTranslations.kr_invite.myInviteCode}: ${controller.kr_referCode.value}',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.copy, size: 20.r, color: Colors.blue),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: controller.kr_referCode.value));
|
||||
KRCommonUtil.kr_showToast(
|
||||
AppTranslations.kr_invite.inviteCodeCopied,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _kr_buildInviteRules(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(16.r),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppTranslations.kr_invite.rules,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Text(
|
||||
AppTranslations.kr_invite.rule1,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Text(
|
||||
AppTranslations.kr_invite.rule2,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ 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';
|
||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
import 'package:kaer_with_panels/app/modules/hi_user_info/controllers/hi_user_info_controller.dart';
|
||||
|
||||
/// 登录类型
|
||||
enum KRLoginProgressStatus {
|
||||
@@ -60,6 +62,9 @@ extension KRLoginTypeExt on KRLoginProgressStatus {
|
||||
|
||||
class KRLoginController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
|
||||
/// 是否注册
|
||||
RxBool kr_isRegistered = false.obs;
|
||||
|
||||
@@ -67,7 +72,7 @@ class KRLoginController extends GetxController
|
||||
var kr_loginType = KRLoginType.kr_email.obs;
|
||||
|
||||
/// 登陆进度状态
|
||||
var kr_loginStatus = KRLoginProgressStatus.kr_check.obs;
|
||||
var kr_loginStatus = KRLoginProgressStatus.kr_registerSendCode.obs;
|
||||
|
||||
/// 验证码倒计时
|
||||
var _countdown = 60; // 倒计时初始值
|
||||
@@ -138,7 +143,7 @@ class KRLoginController extends GetxController
|
||||
case KRLoginProgressStatus.kr_registerSendCode:
|
||||
return AppTranslations.kr_login.next;
|
||||
case KRLoginProgressStatus.kr_registerSetPsd:
|
||||
return AppTranslations.kr_login.registerNow;
|
||||
return AppTranslations.kr_login.passwordLogin;
|
||||
case KRLoginProgressStatus.kr_forgetPsdSendCode:
|
||||
return AppTranslations.kr_login.next;
|
||||
case KRLoginProgressStatus.kr_forgetPsdSetPsd:
|
||||
@@ -149,7 +154,25 @@ class KRLoginController extends GetxController
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// 1. 检查并获取路由参数
|
||||
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
|
||||
final String? entry = Get.arguments['entry'];
|
||||
print('entry: $entry');
|
||||
// 2. 根据 entry 的值设置初始的 kr_loginStatus
|
||||
if (entry == 'forget_psd') {
|
||||
// 如果是从“忘记密码”进入,直接设置为“忘记密码-发送验证码”状态
|
||||
kr_loginStatus.value = KRLoginProgressStatus.kr_forgetPsdSendCode;
|
||||
} else if (entry == 'bind_email') {
|
||||
// 如果是从“绑定邮箱”进入,直接设置为“注册-发送验证码”状态
|
||||
kr_loginStatus.value = KRLoginProgressStatus.kr_registerSetPsd;
|
||||
}else if (entry == 'login') {
|
||||
// 登录
|
||||
kr_loginStatus.value = KRLoginProgressStatus.kr_loginByPsd;
|
||||
}
|
||||
// 如果没有匹配的 entry 值,则保持默认的 kr_loginByPsd 状态
|
||||
}
|
||||
|
||||
|
||||
// 初始化计时器
|
||||
_timer = Timer(Duration.zero, () {});
|
||||
|
||||
@@ -244,12 +267,12 @@ class KRLoginController extends GetxController
|
||||
});
|
||||
|
||||
// 语言变化时更新所有翻译文本
|
||||
ever(KRLanguageUtils.kr_language, (_) {
|
||||
/*ever(KRLanguageUtils.kr_language, (_) {
|
||||
if (kr_canSendCode.value) {
|
||||
kr_countdownText.value = "";
|
||||
kr_countdownText.value = AppTranslations.kr_login.sendCode;
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
kr_initFocus();
|
||||
}
|
||||
@@ -280,9 +303,13 @@ class KRLoginController extends GetxController
|
||||
void kr_sendCode() async {
|
||||
final either = await KRAuthApi().kr_sendCode(
|
||||
accountController.text,
|
||||
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
|
||||
2
|
||||
); // 重置密码验证码类型为3
|
||||
/*
|
||||
*
|
||||
* kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
|
||||
? 2 // 注册验证码类型为2
|
||||
: 3); // 重置密码验证码类型为3
|
||||
: 3*/
|
||||
either.fold((l) {
|
||||
KRCommonUtil.kr_showToast(l.msg);
|
||||
}, (r) async {
|
||||
@@ -316,25 +343,25 @@ class KRLoginController extends GetxController
|
||||
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;
|
||||
}
|
||||
// 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();
|
||||
// final siteConfig = KRSiteConfigService();
|
||||
// final needVerification = siteConfig.isEmailVerificationEnabled() ||
|
||||
// siteConfig.isRegisterVerificationEnabled();
|
||||
|
||||
if (needVerification && codeController.text.isEmpty) {
|
||||
if (codeController.text.isEmpty) {
|
||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
|
||||
return;
|
||||
}
|
||||
@@ -348,7 +375,8 @@ class KRLoginController extends GetxController
|
||||
KRCommonUtil.kr_showToast(l.msg);
|
||||
}, (r) async {
|
||||
_saveLoginData(r);
|
||||
KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
|
||||
// KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
|
||||
KRCommonUtil.kr_showToast('登录成功');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -460,7 +488,8 @@ class KRLoginController extends GetxController
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
KREventBus().kr_sendMessage(KRMessageType.kr_payment);
|
||||
});
|
||||
|
||||
final controller = Get.find<HIUserInfoController>();
|
||||
controller.loadDeviceList();
|
||||
// 登录成功后返回到上一页
|
||||
Get.back();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../../model/response/kr_message_list.dart';
|
||||
import '../../../services/api_service/kr_api.user.dart';
|
||||
@@ -16,13 +17,18 @@ class KRMessageController extends GetxController {
|
||||
final RxBool kr_hasMore = true.obs;
|
||||
int kr_page = 1;
|
||||
final int kr_size = 10;
|
||||
final EasyRefreshController refreshController = EasyRefreshController();
|
||||
final EasyRefreshController refreshController = EasyRefreshController(
|
||||
controlFinishRefresh: true, // ⭐ 必须设置为 true
|
||||
controlFinishLoad: true, // 加载更多通常也需要手动控制
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
kr_getMessageList();
|
||||
// kr_getMessageList();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
refreshController.callRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
|
||||
@@ -8,229 +8,83 @@ 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';
|
||||
|
||||
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_collapsible_list.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/hi_fixed_scrollbar.dart';
|
||||
import '../../../widgets/kr_simple_loading.dart';
|
||||
class KRMessageView extends GetView<KRMessageController> {
|
||||
const KRMessageView({super.key});
|
||||
|
||||
@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(),
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
return HIBaseScaffold(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 主要内容区域
|
||||
Obx(() {
|
||||
return EasyRefresh(
|
||||
controller: controller.refreshController,
|
||||
onRefresh: controller.kr_onRefresh,
|
||||
onLoad: controller.kr_onLoadMore,
|
||||
header: ClassicHeader(
|
||||
dragText: '下拉刷新',
|
||||
armedText: '释放刷新',
|
||||
readyText: '正在刷新...',
|
||||
processingText: '正在刷新...',
|
||||
processedText: '刷新成功',
|
||||
failedText: '刷新失败',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
title: Text(
|
||||
AppTranslations.kr_message.title,
|
||||
style: KrAppTextStyle(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
// 3. 添加 Footer 以显示上拉加载的UI提示
|
||||
footer: ClassicFooter(
|
||||
dragText: '上拉加载',
|
||||
armedText: '释放加载',
|
||||
readyText: '正在加载...',
|
||||
processingText: '正在加载...',
|
||||
processedText: '加载成功',
|
||||
failedText: '加载失败',
|
||||
noMoreText: '没有更多数据了',
|
||||
messageText: '最后更新于 %T',
|
||||
textStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||
messageStyle: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12.sp),
|
||||
iconTheme: IconThemeData(color: Colors.white.withOpacity(0.7)),
|
||||
),
|
||||
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: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w, bottom: 90.w), // 为HIHelpEntrance留出空间
|
||||
child: HiFixedScrollbar(
|
||||
controller: scrollController,
|
||||
isShowScrollbar: controller.kr_messages.length > 0,
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.w),
|
||||
itemCount: controller.kr_messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = controller.kr_messages[index];
|
||||
return _kr_buildMessageCard(message, context);
|
||||
final collapsibleItemData = HICollapsibleItem(
|
||||
title: message.title,
|
||||
content: [message.content],
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: HICollapsibleItemWidget(item: collapsibleItemData),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// 构建消息卡片
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
},),
|
||||
// 底部帮助入口
|
||||
const HIHelpEntrance(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 构建消息内容
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
|
||||
/// 订单状态视图
|
||||
class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||
@@ -130,10 +131,10 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 返回到首页(清空导航栈)
|
||||
Get.offAllNamed('/');
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(Get.context!).primaryColor,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
@@ -145,42 +146,42 @@ class KROrderStatusView extends GetView<KROrderStatusController> {
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
// 查看订阅按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48.h,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
// 返回到首页并切换到订阅页面
|
||||
Get.offAllNamed('/');
|
||||
// 可以通过事件总线发送消息切换tab
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(Get.context!).primaryColor,
|
||||
side: BorderSide(
|
||||
color: Theme.of(Get.context!).primaryColor,
|
||||
width: 1.5,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_orderStatus.viewSubscription,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(Get.context!).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// SizedBox(
|
||||
// width: double.infinity,
|
||||
// height: 48.h,
|
||||
// child: OutlinedButton(
|
||||
// onPressed: () {
|
||||
// // 返回到首页并切换到订阅页面
|
||||
// Get.offAllNamed('/');
|
||||
// // 可以通过事件总线发送消息切换tab
|
||||
// },
|
||||
// style: OutlinedButton.styleFrom(
|
||||
// foregroundColor: Theme.of(Get.context!).primaryColor,
|
||||
// side: BorderSide(
|
||||
// color: Theme.of(Get.context!).primaryColor,
|
||||
// width: 1.5,
|
||||
// ),
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12.r),
|
||||
// ),
|
||||
// ),
|
||||
// child: Text(
|
||||
// AppTranslations.kr_orderStatus.viewSubscription,
|
||||
// style: KrAppTextStyle(
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: Colors.black,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+72
-50
@@ -43,7 +43,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
final kr_isDescriptionExpanded = false.obs;
|
||||
|
||||
/// 当前余额
|
||||
var _kr_balance = 0;
|
||||
RxInt _kr_balance = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -114,20 +114,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
|
||||
/// 初始化用户信息
|
||||
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;
|
||||
_kr_balance = KRAppRunData().kr_balance;
|
||||
}
|
||||
|
||||
/// 获取用户已订阅套餐
|
||||
@@ -297,7 +284,6 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
// 查找余额支付方式的索引
|
||||
final balanceMethodIndex =
|
||||
kr_paymentMethods.indexWhere((method) => method.platform == 'balance');
|
||||
|
||||
// 如果找到余额支付方式且余额足够,选择余额支付
|
||||
if (balanceMethodIndex != -1 && selectedPrice <= _kr_balance / 100) {
|
||||
kr_selectPaymentMethod(balanceMethodIndex);
|
||||
@@ -332,7 +318,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
/// 开始订阅流程
|
||||
Future<void> kr_startSubscription() async {
|
||||
if (!kr_validateSubscriptionData()) return;
|
||||
|
||||
print('开始订阅流程,选定支付');
|
||||
kr_errorMessage.value = '';
|
||||
|
||||
try {
|
||||
@@ -361,6 +347,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
|
||||
/// 处理购买和结账流程
|
||||
Future<void> kr_processPurchaseAndCheckout() async {
|
||||
|
||||
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
|
||||
|
||||
// ⚠️ 支付方式接口暂未实现,使用默认支付方式 ID (1) 和平台 'alipay'
|
||||
@@ -377,12 +364,13 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
// 判断是续订还是新购
|
||||
final isRenewal = _kr_alreadySubscribe
|
||||
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
|
||||
|
||||
final subscribeId = isRenewal
|
||||
? _kr_alreadySubscribe
|
||||
.firstWhere(
|
||||
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id)
|
||||
.userSubscribeId
|
||||
.firstWhere(
|
||||
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id,
|
||||
orElse: () => KRAlreadySubscribe(userSubscribeId: 0, subscribeId: 0), // 默认值
|
||||
)
|
||||
.userSubscribeId
|
||||
: 0;
|
||||
|
||||
// 根据判断结果调用不同的接口
|
||||
@@ -400,18 +388,8 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
'',
|
||||
);
|
||||
|
||||
// 打印下单请求和响应
|
||||
print('═══════════════════════════════════════');
|
||||
print('🔗 下单接口: /v1/public/order/purchase');
|
||||
print('📤 请求参数:');
|
||||
print(' subscribe_id: ${selectedPlan.kr_id}');
|
||||
print(' quantity: $quantity');
|
||||
print(' payment: $paymentMethodId');
|
||||
print(' coupon: ""');
|
||||
print('');
|
||||
|
||||
purchaseEither.fold(
|
||||
(error) {
|
||||
(error) {
|
||||
print('❌ 请求失败:');
|
||||
print(' 错误码: ${error.code}');
|
||||
print(' 错误信息: ${error.msg}');
|
||||
@@ -419,6 +397,19 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
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,
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
print('✅ 请求成功:');
|
||||
print(' 订单号: $order');
|
||||
print('═══════════════════════════════════════');
|
||||
@@ -446,6 +437,19 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
return plan.kr_unitPrice / 100;
|
||||
}
|
||||
|
||||
/// 获取套餐每天价格
|
||||
double kr_getDayPlanPrice(KRPackageListItem plan, {int? discountIndex}) {
|
||||
if (discountIndex != null &&
|
||||
discountIndex >= 0 &&
|
||||
discountIndex < plan.kr_discount.length) {
|
||||
// 计算折扣价格
|
||||
final discount = plan.kr_discount[discountIndex];
|
||||
return (plan.kr_unitPrice / 100) *
|
||||
(discount.kr_discount / 100);
|
||||
}
|
||||
return plan.kr_unitPrice / 100;
|
||||
}
|
||||
|
||||
/// 获取时间字符串
|
||||
/// 根据 quantity 和 unit_time 转换成多语言描述
|
||||
/// Day: 7天 -> "一周",30天 -> "一个月",90天 -> "一个季度",180天 -> "半年",365天 -> "一年"
|
||||
@@ -459,20 +463,21 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
|
||||
// 如果是 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);
|
||||
}
|
||||
return AppTranslations.kr_purchaseMembership.days(quantity);
|
||||
// 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) {
|
||||
@@ -504,7 +509,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
}
|
||||
// 计算折扣百分比(例如:97% 显示为 -3%)
|
||||
final discountPercent = 100 - discount.kr_discount;
|
||||
return '-${discountPercent}%';
|
||||
return '${discountPercent}%';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -621,14 +626,14 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
final checkoutEither = await _kr_subscribeApi.kr_checkout(orderNo);
|
||||
|
||||
checkoutEither.fold(
|
||||
(error) {
|
||||
(error) {
|
||||
print('❌ Checkout 失败:');
|
||||
print(' 错误码: ${error.code}');
|
||||
print(' 错误信息: ${error.msg}');
|
||||
print('═══════════════════════════════════════');
|
||||
KRCommonUtil.kr_showToast(error.msg);
|
||||
},
|
||||
(checkoutResponse) async {
|
||||
(checkoutResponse) async {
|
||||
print('✅ Checkout 成功:');
|
||||
print(' 支付类型: ${checkoutResponse.type}');
|
||||
|
||||
@@ -697,10 +702,27 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
'stripe': checkoutResponse.stripe,
|
||||
},
|
||||
);
|
||||
} else if (checkoutResponse.type == 'balance') {
|
||||
// Stripe 类型:显示 Stripe 支付表单
|
||||
print(' Stripe Method: ${checkoutResponse.stripe?.method}');
|
||||
print(' Stripe Client Secret: ${checkoutResponse.stripe?.clientSecret}');
|
||||
print('💳 显示 Stripe 支付表单...');
|
||||
print('═══════════════════════════════════════');
|
||||
|
||||
Get.toNamed(
|
||||
Routes.KR_ORDER_STATUS,
|
||||
arguments: {
|
||||
'order': orderNo,
|
||||
'payment_type': paymentPlatform,
|
||||
'uri': '',
|
||||
'checkout_type': 'balance',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
print('⚠️ 未知的支付类型: ${checkoutResponse.type}');
|
||||
print('═══════════════════════════════════════');
|
||||
KRCommonUtil.kr_showToast('不支持的支付类型: ${checkoutResponse.type}');
|
||||
// KRCommonUtil.kr_showToast('不支持的支付类型: ${checkoutResponse.type}');
|
||||
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math';
|
||||
@@ -12,8 +13,15 @@ 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/utils/kr_secure_storage.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
|
||||
import 'package:flutter/material.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';
|
||||
import 'package:kaer_with_panels/app/modules/kr_home/models/kr_home_views_status.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class KRSplashController extends GetxController {
|
||||
@@ -26,6 +34,9 @@ class KRSplashController extends GetxController {
|
||||
// 错误信息
|
||||
final RxString kr_errorMessage = ''.obs;
|
||||
|
||||
/// 订阅服务
|
||||
final KRSubscribeService kr_subscribeService = KRSubscribeService();
|
||||
|
||||
// 倒计时
|
||||
// final count = 0.obs;
|
||||
// 是否正在加载
|
||||
@@ -33,49 +44,73 @@ class KRSplashController extends GetxController {
|
||||
// // 是否初始化成功
|
||||
// final isInitialized = false.obs;
|
||||
|
||||
// 启动开始时间(用于计算总耗时)
|
||||
late final DateTime _startTime;
|
||||
late DateTime _stepStartTime;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// 记录启动开始时间
|
||||
_startTime = DateTime.now();
|
||||
_stepStartTime = _startTime;
|
||||
|
||||
// 使用 print 确保日志一定会输出
|
||||
print('═══════════════════════════════════════');
|
||||
print('🎬 KRSplashController onInit 被调用了!');
|
||||
print('═══════════════════════════════════════');
|
||||
print('[SPLASH_TIMING] ═══════════════════════════════════════');
|
||||
print('[SPLASH_TIMING] 🎬 KRSplashController onInit 被调用了!');
|
||||
print('[SPLASH_TIMING] ⏰ 启动开始时间: ${_startTime.toIso8601String()}');
|
||||
print('[SPLASH_TIMING] ═══════════════════════════════════════');
|
||||
|
||||
KRLogUtil.kr_i('═══════════════════════════════════════', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('🎬 启动页控制器 onInit', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('═══════════════════════════════════════', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ═══════════════════════════════════════', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🎬 启动页控制器 onInit', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ═══════════════════════════════════════', tag: 'SplashController');
|
||||
|
||||
// 立即初始化网站配置(在任何其他逻辑之前)
|
||||
print('🌐 准备调用 _kr_initSiteConfig...');
|
||||
// 必须等待网站配置和设备登录完成后,再进行后续初始化
|
||||
// 异步初始化网站配置
|
||||
_kr_initSiteConfig().then((_) {
|
||||
print('🔧 网站配置完成,开始调用 _kr_initialize...');
|
||||
_kr_initialize();
|
||||
});
|
||||
}
|
||||
|
||||
/// 初始化网站配置(最优先执行)
|
||||
Future<void> _kr_initSiteConfig() async {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('🌐 【最优先】开始初始化网站配置...');
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
// 记录步骤耗时的辅助方法
|
||||
void _logStepTiming(String stepName) {
|
||||
final now = DateTime.now();
|
||||
final stepDuration = now.difference(_stepStartTime);
|
||||
final totalDuration = now.difference(_startTime);
|
||||
final stepMs = stepDuration.inMilliseconds;
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('[SPLASH_TIMING] ⏱️ $stepName 耗时: ${stepMs}ms | 累计: ${totalMs}ms');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ⏱️ $stepName 耗时: ${stepMs}ms | 累计: ${totalMs}ms', tag: 'SplashController');
|
||||
|
||||
_stepStartTime = now; // 更新步骤开始时间
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('🌐 【最优先】初始化网站配置...', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
/// 初始化网站配置(异步执行)
|
||||
Future<void> _kr_initSiteConfig() async {
|
||||
print('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('[SPLASH_TIMING] 🌐 开始初始化网站配置...');
|
||||
print('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🌐 初始化网站配置...', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
|
||||
try {
|
||||
print('📞 准备调用 KRSiteConfigService().initialize()...');
|
||||
final success = await KRSiteConfigService().initialize();
|
||||
final crispId = await KRSiteConfigService().getCrispId();
|
||||
print('📞 KRSiteConfigService().initialize() 返回: $success');
|
||||
|
||||
if (success) {
|
||||
print('✅ 网站配置初始化成功');
|
||||
KRLogUtil.kr_i('✅ 网站配置初始化成功', tag: 'SplashController');
|
||||
final config = AppConfig.getInstance();
|
||||
config.kr_website_id = crispId;
|
||||
print('📞 KRSiteConfigService().initialize() 返回: $crispId');
|
||||
print('AppConfig website_id 已更新为: ${config.kr_website_id}');
|
||||
|
||||
// 检查是否支持设备登录
|
||||
await _kr_checkAndPerformDeviceLogin();
|
||||
print('[SPLASH_TIMING] ✅ 网站配置初始化成功');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ✅ 网站配置初始化成功', tag: 'SplashController');
|
||||
} else {
|
||||
print('⚠️ 网站配置初始化失败,将使用默认配置');
|
||||
KRLogUtil.kr_w('⚠️ 网站配置初始化失败,将使用默认配置', tag: 'SplashController');
|
||||
@@ -89,98 +124,57 @@ class KRSplashController extends GetxController {
|
||||
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');
|
||||
_logStepTiming('开始主要初始化流程');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🔧 开始执行 _kr_initialize', tag: 'SplashController');
|
||||
|
||||
// 只在手机端检查网络权限
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
KRLogUtil.kr_i('📱 移动平台,检查网络权限...', tag: 'SplashController');
|
||||
_logStepTiming('开始网络权限检查');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 📱 移动平台,检查网络权限...', tag: 'SplashController');
|
||||
final bool hasNetworkPermission = await KRNetworkCheck.kr_initialize(
|
||||
Get.context!,
|
||||
onPermissionGranted: () async {
|
||||
_logStepTiming('网络权限检查完成');
|
||||
await _kr_continueInitialization();
|
||||
},
|
||||
);
|
||||
|
||||
if (!hasNetworkPermission) {
|
||||
// 计算网络权限失败时的总耗时
|
||||
final endTime = DateTime.now();
|
||||
final totalDuration = endTime.difference(_startTime);
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('❌ 网络权限失败时间: ${endTime.toIso8601String()}');
|
||||
print('🕐 网络权限失败总耗时: ${totalMs}ms (${totalDuration.inSeconds}.${(totalMs % 1000).toString().padLeft(3, '0')}s)');
|
||||
|
||||
kr_hasError.value = true;
|
||||
kr_errorMessage.value = AppTranslations.kr_splash.kr_networkPermissionFailed;
|
||||
KRLogUtil.kr_e('❌ 网络权限检查失败', tag: 'SplashController');
|
||||
KRLogUtil.kr_e('⏰ 网络权限失败总耗时: ${totalMs}ms', tag: 'SplashController');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 非手机端直接继续初始化
|
||||
KRLogUtil.kr_i('💻 桌面平台,直接执行初始化', tag: 'SplashController');
|
||||
_logStepTiming('桌面平台跳过权限检查');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 💻 桌面平台,直接执行初始化', tag: 'SplashController');
|
||||
await _kr_continueInitialization();
|
||||
}
|
||||
} catch (e) {
|
||||
// 计算初始化异常时的总耗时
|
||||
final endTime = DateTime.now();
|
||||
final totalDuration = endTime.difference(_startTime);
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('❌ 初始化异常时间: ${endTime.toIso8601String()}');
|
||||
print('🕐 初始化异常总耗时: ${totalMs}ms (${totalDuration.inSeconds}.${(totalMs % 1000).toString().padLeft(3, '0')}s)');
|
||||
|
||||
KRLogUtil.kr_e('❌ _kr_initialize 异常: $e', tag: 'SplashController');
|
||||
KRLogUtil.kr_e('⏰ 初始化异常总耗时: ${totalMs}ms', tag: 'SplashController');
|
||||
kr_hasError.value = true;
|
||||
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
|
||||
}
|
||||
@@ -188,37 +182,77 @@ class KRSplashController extends GetxController {
|
||||
|
||||
Future<void> _kr_continueInitialization() async {
|
||||
try {
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('🚀 启动页主流程开始...', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
_logStepTiming('开始继续初始化流程');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🚀 启动页主流程开始...', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
|
||||
// 只在手机端检查网络连接
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
KRLogUtil.kr_i('📱 检查网络连接...', tag: 'SplashController');
|
||||
_logStepTiming('开始网络连接检查');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 📱 检查网络连接...', tag: 'SplashController');
|
||||
final bool isConnected = await KRNetworkCheck.kr_checkNetworkConnection();
|
||||
if (!isConnected) {
|
||||
// 计算网络连接失败时的总耗时
|
||||
final endTime = DateTime.now();
|
||||
final totalDuration = endTime.difference(_startTime);
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('❌ 网络连接失败时间: ${endTime.toIso8601String()}');
|
||||
print('🕐 网络连接失败总耗时: ${totalMs}ms (${totalDuration.inSeconds}.${(totalMs % 1000).toString().padLeft(3, '0')}s)');
|
||||
|
||||
kr_hasError.value = true;
|
||||
kr_errorMessage.value = AppTranslations.kr_splash.kr_networkConnectionFailed;
|
||||
KRLogUtil.kr_e('❌ 网络连接失败', tag: 'SplashController');
|
||||
KRLogUtil.kr_e('⏰ 网络连接失败总耗时: ${totalMs}ms', tag: 'SplashController');
|
||||
return;
|
||||
}
|
||||
KRLogUtil.kr_i('✅ 网络连接正常', tag: 'SplashController');
|
||||
_logStepTiming('网络连接检查完成');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ✅ 网络连接正常', tag: 'SplashController');
|
||||
} else {
|
||||
KRLogUtil.kr_i('💻 桌面平台,跳过网络连接检查', tag: 'SplashController');
|
||||
_logStepTiming('桌面平台跳过网络检查');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 💻 桌面平台,跳过网络连接检查', tag: 'SplashController');
|
||||
}
|
||||
|
||||
// 网络检查完成后执行设备登录(始终支持)
|
||||
_logStepTiming('开始设备登录检查');
|
||||
// 5️⃣ 执行设备登录
|
||||
final success = await KRAppRunData.getInstance().kr_checkAndPerformDeviceLogin();
|
||||
|
||||
if (!success) {
|
||||
// 设备登录失败 → 提示用户重试
|
||||
HIDialog.show(
|
||||
message: '设备登录失败,请检查网络或重试',
|
||||
confirmText: '重试',
|
||||
preventBackDismiss: true,
|
||||
onConfirm: () async {
|
||||
await _kr_initialize(); // 递归重试
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
_logStepTiming('设备登录检查完成');
|
||||
|
||||
// 初始化配置
|
||||
KRLogUtil.kr_i('⚙️ 开始初始化应用配置...', tag: 'SplashController');
|
||||
await AppConfig().initConfig(
|
||||
onSuccess: () async {
|
||||
// 配置初始化成功,继续后续步骤
|
||||
KRLogUtil.kr_i('✅ 应用配置初始化成功,继续后续步骤', tag: 'SplashController');
|
||||
// 配置初始化成功,没有配置
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ⚙️ 初始化应用配置...', tag: 'SplashController');
|
||||
await _kr_continueAfterConfig();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// 计算配置初始化异常时的总耗时
|
||||
final endTime = DateTime.now();
|
||||
final totalDuration = endTime.difference(_startTime);
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('❌ 配置初始化异常时间: ${endTime.toIso8601String()}');
|
||||
print('🕐 配置初始化异常总耗时: ${totalMs}ms (${totalDuration.inSeconds}.${(totalMs % 1000).toString().padLeft(3, '0')}s)');
|
||||
|
||||
// 配置初始化失败,显示错误信息
|
||||
KRLogUtil.kr_e('❌ 启动页初始化异常: $e', tag: 'SplashController');
|
||||
KRLogUtil.kr_e('⏰ 配置初始化异常总耗时: ${totalMs}ms', tag: 'SplashController');
|
||||
kr_hasError.value = true;
|
||||
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
|
||||
}
|
||||
@@ -228,20 +262,14 @@ class KRSplashController extends GetxController {
|
||||
Future<void> _kr_continueAfterConfig() async {
|
||||
try {
|
||||
// 初始化SingBox
|
||||
_logStepTiming('开始SingBox初始化');
|
||||
await KRSingBoxImp.instance.init();
|
||||
_logStepTiming('SingBox初始化完成');
|
||||
|
||||
// 检查是否已经通过设备登录设置了用户信息
|
||||
// 如果已登录(设备登录已完成),则跳过 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));
|
||||
// 用户信息初始化完成后,立即初始化订阅服务
|
||||
print('[SPLASH_TIMING] 🔄 开始初始化订阅服务...');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🔄 开始初始化订阅服务', tag: 'SplashController');
|
||||
await _kr_ensureSubscribeServiceInitialized();
|
||||
|
||||
// 验证登录状态是否已正确设置
|
||||
final loginStatus = KRAppRunData.getInstance().kr_isLogin.value;
|
||||
@@ -257,22 +285,91 @@ class KRSplashController extends GetxController {
|
||||
}
|
||||
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');
|
||||
// 计算总耗时
|
||||
final endTime = DateTime.now();
|
||||
final totalDuration = endTime.difference(_startTime);
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('[SPLASH_TIMING] ⏰ 启动结束时间: ${endTime.toIso8601String()}');
|
||||
print('[SPLASH_TIMING] 🕐 启动总耗时: ${totalMs}ms (${totalDuration.inSeconds}.${(totalMs % 1000).toString().padLeft(3, '0')}s)');
|
||||
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🎯 准备进入主页', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 📊 最终登录状态: $loginStatus', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] 🎫 Token存在: $hasToken', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ⏰ 启动总耗时: ${totalMs}ms', tag: 'SplashController');
|
||||
KRLogUtil.kr_i('[SPLASH_TIMING] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
|
||||
|
||||
// 直接导航到主页(无论是否登录,主页会根据登录状态显示不同内容)
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
_logStepTiming('开始页面导航');
|
||||
if(loginStatus) {
|
||||
// 直接导航到主页
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
}else {
|
||||
kr_hasError.value = true;
|
||||
kr_errorMessage.value = '登录:${AppTranslations.kr_splash.kr_initializationFailed}';
|
||||
}
|
||||
_logStepTiming('页面导航完成');
|
||||
} catch (e) {
|
||||
// 计算错误时的总耗时
|
||||
final endTime = DateTime.now();
|
||||
final totalDuration = endTime.difference(_startTime);
|
||||
final totalMs = totalDuration.inMilliseconds;
|
||||
|
||||
print('❌ 启动失败时间: ${endTime.toIso8601String()}');
|
||||
print('🕐 启动失败总耗时: ${totalMs}ms (${totalDuration.inSeconds}.${(totalMs % 1000).toString().padLeft(3, '0')}s)');
|
||||
|
||||
// 后续步骤失败,显示错误信息
|
||||
KRLogUtil.kr_e('启动初始化失败: $e', tag: 'SplashController');
|
||||
KRLogUtil.kr_e('⏰ 启动失败总耗时: ${totalMs}ms', tag: 'SplashController');
|
||||
kr_hasError.value = true;
|
||||
kr_errorMessage.value = '${AppTranslations.kr_splash.kr_initializationFailed}$e';
|
||||
}
|
||||
}
|
||||
|
||||
/// 确保订阅服务初始化
|
||||
Future<void> _kr_ensureSubscribeServiceInitialized() async {
|
||||
try {
|
||||
// 检查订阅服务状态
|
||||
final currentStatus = kr_subscribeService.kr_currentStatus.value;
|
||||
KRLogUtil.kr_i('订阅服务当前状态: $currentStatus', tag: 'SplashController');
|
||||
|
||||
if (currentStatus == KRSubscribeServiceStatus.kr_none ||
|
||||
currentStatus == KRSubscribeServiceStatus.kr_error) {
|
||||
KRLogUtil.kr_i(
|
||||
'订阅服务未初始化或错误,开始初始化', tag: 'SplashController');
|
||||
|
||||
// 初始化订阅服务并等待完成
|
||||
// 设置加载状态
|
||||
KRHomeController().kr_currentListStatus.value = KRHomeViewsListStatus.kr_loading;
|
||||
try {
|
||||
await kr_subscribeService.kr_refreshAll();
|
||||
KRLogUtil.kr_i('订阅服务初始化完成', tag: 'SplashController');
|
||||
} catch (error) {
|
||||
KRLogUtil.kr_e('订阅服务初始化失败: $error', tag: 'SplashController');
|
||||
// 重新抛出异常,让调用方知道初始化失败
|
||||
KRHomeController().kr_currentListStatus.value = KRHomeViewsListStatus.kr_error;
|
||||
rethrow;
|
||||
}
|
||||
} else if (currentStatus == KRSubscribeServiceStatus.kr_loading) {
|
||||
KRLogUtil.kr_i('订阅服务正在初始化中', tag: 'SplashController');
|
||||
// 如果正在加载中,等待加载完成
|
||||
while (kr_subscribeService.kr_currentStatus.value == KRSubscribeServiceStatus.kr_loading) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
}
|
||||
KRLogUtil.kr_i('订阅服务加载完成,最终状态: ${kr_subscribeService.kr_currentStatus.value}', tag: 'SplashController');
|
||||
KRHomeController().kr_currentListStatus.value = KRHomeViewsListStatus.kr_loading;
|
||||
} else if (currentStatus == KRSubscribeServiceStatus.kr_success) {
|
||||
KRHomeController().kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
KRLogUtil.kr_i('订阅服务已成功初始化', tag: 'SplashController');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('确保订阅服务初始化失败: $e', tag: 'SplashController');
|
||||
KRHomeController().kr_currentListStatus.value = KRHomeViewsListStatus.kr_error;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// 重试按钮点击事件
|
||||
void kr_retry() {
|
||||
kr_hasError.value = false;
|
||||
@@ -289,4 +386,4 @@ class KRSplashController extends GetxController {
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,32 +36,11 @@ class KRSplashView extends GetView<KRSplashController> {
|
||||
children: [
|
||||
// 图片区域
|
||||
KrLocalImage(
|
||||
imageName: "splash_illustration",
|
||||
width: 218.w,
|
||||
height: 194.w,
|
||||
imageName: "splash_illustration-hi",
|
||||
width: 286.w,
|
||||
height: 101.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(() {
|
||||
@@ -79,8 +58,8 @@ class KRSplashView extends GetView<KRSplashController> {
|
||||
ElevatedButton(
|
||||
onPressed: controller.kr_retry,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.black,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 32.w,
|
||||
vertical: 12.h,
|
||||
@@ -99,7 +78,7 @@ class KRSplashView extends GetView<KRSplashController> {
|
||||
return Column(
|
||||
children: [
|
||||
KRSimpleLoading(
|
||||
color: Colors.blue,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 24.0,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
@@ -107,7 +86,7 @@ class KRSplashView extends GetView<KRSplashController> {
|
||||
AppTranslations.kr_splash.initializing,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.textTheme.bodySmall?.color,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -213,7 +213,7 @@ class KRUserInfoController extends GetxController with KRAppBarOpacityMixin {
|
||||
// );
|
||||
|
||||
// 使用 AppConfig 中的固定余额
|
||||
kr_balance.value = AppConfig.kr_userBalance.toDouble() / 100;
|
||||
kr_balance.value = KRAppRunData().kr_balance.toDouble() / 100;
|
||||
}
|
||||
|
||||
/// 处理用户退出登录
|
||||
|
||||
Reference in New Issue
Block a user