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:
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user