初始化提交

This commit is contained in:
2025-09-23 16:23:15 +08:00
commit 877b18a70f
590 changed files with 53617 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
class KRDialog extends StatelessWidget {
final String? title;
final String? message;
final String? confirmText;
final String? cancelText;
final VoidCallback? onConfirm;
final VoidCallback? onCancel;
final Widget? icon;
final Widget? customMessageWidget;
const KRDialog({
Key? key,
this.title,
this.message,
this.confirmText,
this.cancelText,
this.onConfirm,
this.onCancel,
this.icon,
this.customMessageWidget,
}) : super(key: key);
static Future<void> show({
String? title,
String? message,
String? confirmText,
String? cancelText,
VoidCallback? onConfirm,
VoidCallback? onCancel,
Widget? icon,
Widget? customMessageWidget,
}) {
return Get.dialog(
KRDialog(
title: title,
message: message,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel,
icon: icon,
customMessageWidget: customMessageWidget,
),
barrierDismissible: false,
);
}
Widget _buildConfirmButton() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(23.r),
boxShadow: [
BoxShadow(
color: const Color(0xFF1797FF).withOpacity(0.25),
blurRadius: 8.r,
offset: Offset(0, 2.w),
),
],
),
child: TextButton(
onPressed: () {
Get.back();
onConfirm?.call();
},
style: TextButton.styleFrom(
backgroundColor: const Color(0xFF1797FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.r),
),
padding: EdgeInsets.zero,
minimumSize: Size.fromHeight(46.w),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
confirmText ?? AppTranslations.kr_dialog.kr_confirm,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Dialog(
backgroundColor: theme.cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
child: Container(
width: 280.w,
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
icon!,
SizedBox(height: 20.w),
],
if (title != null) ...[
Text(
title!,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : const Color(0xFF333333),
fontFamily: 'AlibabaPuHuiTi-Medium',
height: 1.3,
),
textAlign: TextAlign.center,
),
SizedBox(height: 12.w),
],
if (message != null || customMessageWidget != null) ...[
Container(
constraints: BoxConstraints(maxHeight: 200.h),
child: SingleChildScrollView(
child: customMessageWidget ?? Text(
message!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: isDark ? const Color(0xFFCCCCCC) : const Color(0xFF666666),
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
),
),
),
SizedBox(height: 28.w),
],
if (cancelText != null) ...[
Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(23.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 4.r,
offset: Offset(0, 2.w),
),
],
),
child: TextButton(
onPressed: () {
Get.back();
onCancel?.call();
},
style: TextButton.styleFrom(
backgroundColor: isDark ? const Color(0xFF222222) : const Color(0xFFEEEEEE),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(23.r),
),
padding: EdgeInsets.zero,
minimumSize: Size.fromHeight(46.w),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
cancelText ?? AppTranslations.kr_dialog.kr_cancel,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: isDark ? const Color(0xFFBBBBBB) : const Color(0xFF666666),
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
),
),
SizedBox(width: 12.w),
Expanded(child: _buildConfirmButton()),
],
),
] else ...[
_buildConfirmButton(),
],
],
),
),
);
}
}
+168
View File
@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
import '../../localization/app_translations.dart';
class KRUpdateDialog extends StatelessWidget {
final String version;
final String updateContent;
final VoidCallback? onConfirm;
final VoidCallback? onCancel;
final bool showCancelButton;
final bool isForceUpdate;
KRUpdateDialog({
Key? key,
required this.version,
String? updateContent,
this.onConfirm,
this.onCancel,
this.showCancelButton = true,
this.isForceUpdate = false,
}) : updateContent = updateContent ?? AppTranslations.kr_update.defaultContent,
super(key: key);
static Future<void> show({
required String version,
String? updateContent,
VoidCallback? onConfirm,
VoidCallback? onCancel,
bool showCancelButton = true,
bool isForceUpdate = false,
}) {
return Get.dialog(
KRUpdateDialog(
version: version,
updateContent: updateContent,
onConfirm: onConfirm,
onCancel: onCancel,
showCancelButton: showCancelButton,
isForceUpdate: isForceUpdate,
),
barrierDismissible: false,
);
}
Widget _buildConfirmButton(ThemeData theme) {
return Container(
width: double.infinity,
height: 44.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22.r),
gradient: const LinearGradient(
colors: [Color(0xFF2196F3), Color(0xFF1E88E5)],
),
),
child: TextButton(
onPressed: () {
if (!isForceUpdate) {
Get.back();
}
onConfirm?.call();
},
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(22.r),
),
padding: EdgeInsets.zero,
),
child: Text(
AppTranslations.kr_update.updateNow,
style: theme.textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 16.sp,
),
),
),
);
}
Widget _buildCancelButton(ThemeData theme) {
return TextButton(
onPressed: () {
Get.back();
onCancel?.call();
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: Text(
AppTranslations.kr_update.updateLater,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.hintColor,
fontSize: 14.sp,
),
),
);
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Dialog(
backgroundColor: theme.cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
child: Container(
width: 280.w,
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
KrLocalImage(
imageName: 'vs_update',
width: 190.w,
height: 98.w,
imageType: ImageType.svg,
),
SizedBox(height: 16.h),
Text(
AppTranslations.kr_update.title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18.sp,
color: theme.brightness == Brightness.light
? const Color(0xFF333333)
: theme.textTheme.titleLarge?.color,
),
),
SizedBox(height: 4.h),
Text(
'V$version',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.hintColor,
fontSize: 14.sp,
),
),
SizedBox(height: 1.h),
Container(
constraints: BoxConstraints(maxHeight: 120.h),
child: SingleChildScrollView(
child: Text(
updateContent,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.hintColor,
fontSize: 14.sp,
height: 1.5,
),
),
),
),
SizedBox(height: 20.h),
_buildConfirmButton(theme),
if (showCancelButton) ...[
SizedBox(height: 12.h),
_buildCancelButton(theme),
],
],
),
),
);
}
}
+60
View File
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class KrAppTextStyle extends TextStyle {
KrAppTextStyle({
required double fontSize,
FontWeight fontWeight = FontWeight.w400,
Color? color,
}) : super(
fontSize: fontSize.sp,
fontFamily: _getFontFamily(fontWeight),
fontWeight: fontWeight,
color: color,
height: 1.4,
);
// 提供标题文本样式
static KrAppTextStyle titleTextStyle({
required double fontSize,
Color? color,
}) {
return _commonTextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w500,
color: color,
);
}
// 私有方法创建正文文本样式
static KrAppTextStyle _bodyTextStyle({
required double fontSize,
Color? color,
}) {
return _commonTextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w400,
color: color,
);
}
// 私有方法创建常规文本样式
static KrAppTextStyle _commonTextStyle({
required double fontSize,
FontWeight fontWeight = FontWeight.w400,
Color? color,
}) {
return KrAppTextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
color: color,
);
}
// 根据字体粗细获取字体家族
static String _getFontFamily(FontWeight fontWeight) {
return fontWeight == FontWeight.w500
? 'AlibabaPuHuiTi-Medium'
: 'AlibabaPuHuiTi-Regular';
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:country_flags/country_flags.dart';
class KRCountryFlag extends StatelessWidget {
final String countryCode;
final double? width;
final double? height;
final bool isCircle;
final BoxFit fit; // 填充模式
final bool maintainSize; // 是否保持宽高比
final Color? bgColor; // 背景色
final bool clip; // 是否裁剪
const KRCountryFlag({
Key? key,
required this.countryCode,
this.width,
this.height,
this.isCircle = true,
this.fit = BoxFit.cover, // 默认填充
this.maintainSize = true, // 默认保持宽高比
this.bgColor = Colors.white,
this.clip = true, // 默认裁剪
}) : super(key: key);
String _getCountryCode(String code) {
// 处理特殊国家代码
final Map<String, String> specialCases = {
'UK': 'gb',
'USA': 'us',
// 添加其他特殊情况...
};
return specialCases[code.toUpperCase()] ?? code.toLowerCase();
}
@override
Widget build(BuildContext context) {
// 计算实际尺寸
final double actualWidth = width ?? 50.w;
final double actualHeight = maintainSize ? actualWidth : (height ?? 50.w);
Widget flagWidget = CountryFlag.fromCountryCode(
_getCountryCode(countryCode),
width: actualWidth,
height: actualHeight,
);
// 添加填充模式
flagWidget = SizedBox(
width: actualWidth,
height: actualHeight,
child: FittedBox(
fit: fit,
child: flagWidget,
),
);
// 添加背景和形状
flagWidget = Container(
width: actualWidth,
height: actualHeight,
decoration: BoxDecoration(
color: bgColor,
shape: isCircle ? BoxShape.circle : BoxShape.rectangle,
borderRadius: !isCircle ? BorderRadius.circular(8.w) : null,
),
child: flagWidget,
);
// 最后添加裁剪
if (clip && isCircle) {
flagWidget = ClipOval(child: flagWidget);
}
return flagWidget;
}
}
+22
View File
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class KRKeepAliveWrapper extends StatefulWidget {
final Widget child;
const KRKeepAliveWrapper(this.child, {Key? key}) : super(key: key);
@override
_KeepAliveWrapperState createState() => _KeepAliveWrapperState();
}
class _KeepAliveWrapperState extends State<KRKeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
bool get wantKeepAlive => true;
}
+117
View File
@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
/// 语言切换弹框组件
class KRLanguageSwitchDialog extends StatelessWidget {
const KRLanguageSwitchDialog({super.key});
/// 显示语言切换弹框的静态方法
static Future<void> kr_show() async {
final isChineseRegion = await KRLanguageUtils.checkInitialLanguage();
if (isChineseRegion) {
await Get.dialog(
const KRLanguageSwitchDialog(),
barrierDismissible: false,
);
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: 280.w,
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(24.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 图标
KrLocalImage(
imageName: 'language_switch',
width: 120.w,
height: 120.h,
),
SizedBox(height: 16.h),
// 标题
Text(
'根据您所在地区以及您的语言设置是否切换到中文语言?',
textAlign: TextAlign.center,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
SizedBox(height: 24.h),
// 切换按钮
_kr_buildButton(
context: context,
text: '切换',
isPrimary: true,
onTap: () async {
final zhLanguage = KRLanguage.values.firstWhere(
(lang) => lang.countryCode == 'zh',
orElse: () => KRLanguage.zh,
);
await KRLanguageUtils.switchLanguage(zhLanguage);
Get.back();
},
),
SizedBox(height: 12.h),
// 不切换按钮
_kr_buildButton(
context: context,
text: '不切换',
isPrimary: false,
onTap: () => Get.back(),
),
],
),
),
);
}
/// 构建按钮
Widget _kr_buildButton({
required BuildContext context,
required String text,
required bool isPrimary,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
width: double.infinity,
height: 44.h,
decoration: BoxDecoration(
color: isPrimary ? Colors.blue : Colors.transparent,
borderRadius: BorderRadius.circular(22.r),
border: isPrimary
? null
: Border.all(
color: Colors.blue,
width: 1,
),
),
alignment: Alignment.center,
child: Text(
text,
style: KrAppTextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: isPrimary ? Colors.white : Colors.blue,
),
),
),
);
}
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'kr_simple_loading.dart';
/// 加载动画类型
enum KRLoadingType {
/// 波纹动画
kr_ripple,
/// 双波纹动画
kr_doubleBounce,
/// 波浪动画
kr_wave,
/// 脉冲动画
kr_pulse,
/// 旋转动画
kr_rotatingCircle,
/// 折叠动画
kr_foldingCube,
}
/// 自定义加载动画组件
class KRLoadingAnimation extends StatelessWidget {
/// 动画颜色
final Color? color;
/// 动画大小
final double? size;
/// 动画类型
final KRLoadingType type;
/// 构造函数
const KRLoadingAnimation({
Key? key,
this.color = Colors.blue,
this.size,
this.type = KRLoadingType.kr_ripple,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final defaultColor = color ?? Theme.of(context).primaryColor;
final defaultSize = size ?? 15.0;
return _kr_buildAnimation(defaultColor, defaultSize);
}
/// 构建动画
Widget _kr_buildAnimation(Color color, double size) {
switch (type) {
case KRLoadingType.kr_ripple:
return KRSimpleLoading(
color: color,
size: size,
);
case KRLoadingType.kr_doubleBounce:
return KRSimpleLoading(
color: color,
size: size,
);
case KRLoadingType.kr_wave:
return KRSimpleWave(
color: color,
size: size,
);
case KRLoadingType.kr_pulse:
return KRSimplePulse(
color: color,
size: size,
);
case KRLoadingType.kr_rotatingCircle:
return KRSimpleLoading(
color: color,
size: size,
);
case KRLoadingType.kr_foldingCube:
return KRSimpleLoading(
color: color,
size: size,
);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
/// 图片类型枚举
enum ImageType { svg, png, jpg }
class KrLocalImage extends StatelessWidget {
final String imageName; // 不含子目录的纯文件名,例如 "icon"
final ImageType imageType; // 图片类型,可选,默认是 svg
final double? width;
final double? height;
final BoxFit fit;
final Color? color;
const KrLocalImage({
Key? key,
required this.imageName,
this.imageType = ImageType.svg, // 默认类型为 SVG
this.width,
this.height,
this.color,
this.fit = BoxFit.contain,
}) : super(key: key);
@override
Widget build(BuildContext context) {
switch (imageType) {
case ImageType.svg:
return SvgPicture.asset(
'assets/images/$imageName.svg',
width: width,
height: height,
fit: fit,
colorFilter: color != null
? ColorFilter.mode(color!, BlendMode.srcIn)
: null,
);
case ImageType.png:
case ImageType.jpg:
return Image.asset(
'assets/images/$imageName.${imageType == ImageType.png ? 'png' : 'jpg'}',
width: width,
height: height,
fit: fit,
color: color,
colorBlendMode: color != null ? BlendMode.srcIn : null,
);
default:
throw UnsupportedError('Unsupported image type: $imageType');
}
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart';
/// 网络图片加载组件
class KRNetworkImage extends StatelessWidget {
/// 图片URL
final String kr_imageUrl;
/// 图片宽度
final double? kr_width;
/// 图片高度
final double? kr_height;
/// 图片填充方式
final BoxFit kr_fit;
/// 加载中占位组件
final Widget? kr_placeholder;
/// 加载失败占位组件
final Widget? kr_errorWidget;
/// 构造函数
const KRNetworkImage({
Key? key,
required this.kr_imageUrl,
this.kr_width,
this.kr_height,
this.kr_fit = BoxFit.cover,
this.kr_placeholder,
this.kr_errorWidget,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExtendedImage.network(
kr_imageUrl,
width: kr_width,
height: kr_height,
fit: kr_fit,
cache: true,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
return kr_placeholder ??
const Center(child: CircularProgressIndicator());
case LoadState.completed:
return null; // 返回实际图片
case LoadState.failed:
return kr_errorWidget ??
const Icon(Icons.error);
}
},
);
}
}
+96
View File
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
/// 套餐详情弹框
class KRPlanDetailsDialog extends StatelessWidget {
final List<KRFeature> kr_features;
const KRPlanDetailsDialog({
Key? key,
required this.kr_features,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppTranslations.kr_purchaseMembership.planDetails,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: kr_features.length,
itemBuilder: (context, index) {
final feature = kr_features[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.kr_label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
...feature.kr_details.map((detail) => Padding(
padding: const EdgeInsets.only(left: 16, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.check_circle_outline,
size: 16,
color: Colors.green,
),
const SizedBox(width: 8),
Expanded(
child: Text(
detail.kr_description,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
),
],
),
)),
if (index < kr_features.length - 1)
const Divider(height: 24),
],
);
},
),
),
],
),
),
);
}
}
+231
View File
@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
/// 简单的加载动画组件,替代有问题的flutter_spinkit
class KRSimpleLoading extends StatefulWidget {
final Color? color;
final double size;
final Duration duration;
const KRSimpleLoading({
super.key,
this.color,
this.size = 40.0,
this.duration = const Duration(milliseconds: 1000),
});
@override
State<KRSimpleLoading> createState() => _KRSimpleLoadingState();
}
class _KRSimpleLoadingState extends State<KRSimpleLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.rotate(
angle: _animation.value * 2 * 3.14159,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: widget.color ?? Theme.of(context).primaryColor,
width: 2.0,
),
),
child: Padding(
padding: const EdgeInsets.all(2.0),
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(
widget.color ?? Theme.of(context).primaryColor,
),
),
),
),
);
},
);
}
}
/// 脉冲加载动画
class KRSimplePulse extends StatefulWidget {
final Color? color;
final double size;
final Duration duration;
const KRSimplePulse({
super.key,
this.color,
this.size = 40.0,
this.duration = const Duration(milliseconds: 1500),
});
@override
State<KRSimplePulse> createState() => _KRSimplePulseState();
}
class _KRSimplePulseState extends State<KRSimplePulse>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: 0.5 + (_animation.value * 0.5),
child: Opacity(
opacity: 1.0 - _animation.value,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.color ?? Theme.of(context).primaryColor,
),
),
),
);
},
);
}
}
/// 波浪加载动画
class KRSimpleWave extends StatefulWidget {
final Color? color;
final double size;
final Duration duration;
const KRSimpleWave({
super.key,
this.color,
this.size = 40.0,
this.duration = const Duration(milliseconds: 1200),
});
@override
State<KRSimpleWave> createState() => _KRSimpleWaveState();
}
class _KRSimpleWaveState extends State<KRSimpleWave>
with TickerProviderStateMixin {
late List<AnimationController> _controllers;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_controllers = List.generate(3, (index) {
return AnimationController(
duration: widget.duration,
vsync: this,
);
});
_animations = _controllers.map((controller) {
return Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
));
}).toList();
for (int i = 0; i < _controllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 200), () {
if (mounted) {
_controllers[i].repeat();
}
});
}
}
@override
void dispose() {
for (var controller in _controllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
return AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2.0),
width: widget.size / 6,
height: widget.size * (0.3 + (_animations[index].value * 0.7)),
decoration: BoxDecoration(
color: widget.color ?? Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(2.0),
),
);
},
);
}),
);
}
}
+176
View File
@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'kr_simple_loading.dart';
/// 简单的Toast组件,替代flutter_easyloading
class KRToast {
static OverlayEntry? _overlayEntry;
static Timer? _timer;
/// 显示Toast消息
static void kr_showToast(
String message, {
Duration duration = const Duration(milliseconds: 1500),
KRToastPosition position = KRToastPosition.center,
}) {
_hideToast(); // 隐藏之前的Toast
_overlayEntry = OverlayEntry(
builder: (context) => _ToastWidget(
message: message,
position: position,
),
);
Overlay.of(Get.overlayContext!).insert(_overlayEntry!);
_timer = Timer(duration, () {
_hideToast();
});
}
/// 显示加载动画
static void kr_showLoading({String? message}) {
_hideToast(); // 隐藏之前的Toast
_overlayEntry = OverlayEntry(
builder: (context) => _LoadingWidget(
message: message,
),
);
Overlay.of(Get.overlayContext!).insert(_overlayEntry!);
}
/// 隐藏Toast或Loading
static void kr_hideLoading() {
_hideToast();
}
/// 隐藏Toast
static void _hideToast() {
_timer?.cancel();
_timer = null;
_overlayEntry?.remove();
_overlayEntry = null;
}
}
/// Toast位置枚举
enum KRToastPosition {
top,
center,
bottom,
}
/// Toast组件
class _ToastWidget extends StatelessWidget {
final String message;
final KRToastPosition position;
const _ToastWidget({
required this.message,
required this.position,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: SafeArea(
child: Align(
alignment: _getAlignment(),
child: Container(
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(12.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 8,
spreadRadius: 2,
)
],
),
child: Text(
message,
style: const TextStyle(
fontSize: 15.0,
color: Colors.white,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
),
),
),
),
);
}
Alignment _getAlignment() {
switch (position) {
case KRToastPosition.top:
return Alignment.topCenter;
case KRToastPosition.center:
return Alignment.center;
case KRToastPosition.bottom:
return Alignment.bottomCenter;
}
}
}
/// Loading组件
class _LoadingWidget extends StatelessWidget {
final String? message;
const _LoadingWidget({this.message});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.black.withOpacity(0.2),
child: SafeArea(
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 25.0, vertical: 15.0),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(15.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 8,
spreadRadius: 2,
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
KRSimpleLoading(
color: Colors.white,
size: 35.0,
),
if (message != null) ...[
const SizedBox(height: 12.0),
Text(
message!,
style: const TextStyle(
fontSize: 14.0,
color: Colors.white,
),
),
],
],
),
),
),
),
);
}
}