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

This commit is contained in:
2025-10-30 04:47:53 -07:00
parent 145832093e
commit f42a481452
134 changed files with 8032 additions and 4270 deletions
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,
),
),
);
}
}
+199 -305
View File
@@ -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,
),
),
);
},
);
}
}