初始化提交
This commit is contained in:
Executable
+197
@@ -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
@@ -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),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+60
@@ -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';
|
||||
}
|
||||
}
|
||||
Executable
+79
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+88
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+52
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+57
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+96
@@ -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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+231
@@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+176
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user