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:
Executable
+241
@@ -0,0 +1,241 @@
|
||||
import 'dart:ui';
|
||||
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 HIDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
final String? message;
|
||||
final String? confirmText;
|
||||
final String? cancelText;
|
||||
final VoidCallback? onConfirm;
|
||||
final VoidCallback? onCancel;
|
||||
final Widget? customMessageWidget;
|
||||
|
||||
/// 是否允许点击外部关闭
|
||||
final bool barrierDismissible;
|
||||
|
||||
/// 是否禁止物理返回键关闭
|
||||
final bool preventBackDismiss;
|
||||
|
||||
/// 是否点击确认后自动关闭(默认 true)
|
||||
final bool autoClose;
|
||||
|
||||
/// 是否显示按钮 loading(autoClose=false 时自动启用)
|
||||
final bool? showLoading;
|
||||
|
||||
const HIDialog({
|
||||
Key? key,
|
||||
this.title,
|
||||
this.message,
|
||||
this.confirmText,
|
||||
this.cancelText,
|
||||
this.onConfirm,
|
||||
this.onCancel,
|
||||
this.customMessageWidget,
|
||||
this.barrierDismissible = true,
|
||||
this.preventBackDismiss = false,
|
||||
this.autoClose = true,
|
||||
this.showLoading,
|
||||
}) : super(key: key);
|
||||
|
||||
static Future<void> show({
|
||||
String? title,
|
||||
String? message,
|
||||
String? confirmText,
|
||||
String? cancelText,
|
||||
VoidCallback? onConfirm,
|
||||
VoidCallback? onCancel,
|
||||
Widget? customMessageWidget,
|
||||
bool barrierDismissible = true,
|
||||
bool preventBackDismiss = false,
|
||||
bool autoClose = true,
|
||||
bool? showLoading,
|
||||
}) {
|
||||
return Get.dialog(
|
||||
HIDialog(
|
||||
title: title,
|
||||
message: message,
|
||||
confirmText: confirmText,
|
||||
cancelText: cancelText,
|
||||
onConfirm: onConfirm,
|
||||
onCancel: onCancel,
|
||||
customMessageWidget: customMessageWidget,
|
||||
barrierDismissible: barrierDismissible,
|
||||
preventBackDismiss: preventBackDismiss,
|
||||
autoClose: autoClose,
|
||||
showLoading: showLoading,
|
||||
),
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<HIDialog> createState() => _HIDialogState();
|
||||
}
|
||||
|
||||
class _HIDialogState extends State<HIDialog> {
|
||||
bool _isLoading = false;
|
||||
|
||||
Future<void> _handleConfirm() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
final useLoading = widget.showLoading ?? !widget.autoClose;
|
||||
if (useLoading) {
|
||||
setState(() => _isLoading = true);
|
||||
}
|
||||
|
||||
if (widget.autoClose) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
await Future.microtask(() => widget.onConfirm?.call());
|
||||
|
||||
if (useLoading && mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildConfirmButton() {
|
||||
return TextButton(
|
||||
onPressed: _isLoading ? null : _handleConfirm,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFADFF5B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(23.r),
|
||||
),
|
||||
minimumSize: Size.fromHeight(40.w),
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: _isLoading
|
||||
? SizedBox(
|
||||
height: 20.w,
|
||||
width: 20.w,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.black,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.confirmText ?? AppTranslations.kr_dialog.kr_confirm,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dialog = Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: EdgeInsets.all(20.w),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(34.r),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26), // 毛玻璃模糊
|
||||
child: Container(
|
||||
width: 245.w,
|
||||
padding: EdgeInsets.fromLTRB(30.w, 16.w, 30.w, 16.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xF5F5F5).withOpacity(0.6), // 半透明底色
|
||||
borderRadius: BorderRadius.circular(34.r),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2), // 高光边框
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.title != null) ...[
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
widget.title!,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.w),
|
||||
],
|
||||
if (widget.message != null || widget.customMessageWidget != null) ...[
|
||||
Container(
|
||||
constraints: BoxConstraints(maxHeight: 200.h),
|
||||
child: SingleChildScrollView(
|
||||
child: widget.customMessageWidget ??
|
||||
Text(
|
||||
widget.message!,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black,
|
||||
height: 1.4,
|
||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.confirmText != null || widget.cancelText != null) ...[
|
||||
SizedBox(height: 28.w),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.confirmText != null) Expanded(child: _buildConfirmButton()),
|
||||
if (widget.cancelText != null && widget.confirmText != null)
|
||||
SizedBox(width: 12.w),
|
||||
if (widget.cancelText != null)
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
widget.onCancel?.call();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF00B7),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(23.r),
|
||||
),
|
||||
minimumSize: Size.fromHeight(40.w),
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(
|
||||
widget.cancelText!,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
fontFamily: 'AlibabaPuHuiTi-Medium',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.preventBackDismiss) {
|
||||
return WillPopScope(onWillPop: () async => false, child: dialog);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
|
||||
/// 🔹 HIBaseScaffold
|
||||
/// 用于统一页面结构:
|
||||
/// ✅ 全屏背景图
|
||||
/// ✅ 左上角返回按钮
|
||||
/// ✅ 右上角套餐入口按钮(球形)
|
||||
/// ✅ 自定义主体内容(body)
|
||||
///
|
||||
/// 使用:
|
||||
/// ```dart
|
||||
/// return HIBaseScaffold(
|
||||
/// showBack: true,
|
||||
/// showSubscriptionCorner: true,
|
||||
/// body: YourWidget(),
|
||||
/// );
|
||||
/// ```
|
||||
class HIBaseScaffold extends StatelessWidget {
|
||||
/// 页面主体内容
|
||||
final Widget child;
|
||||
|
||||
/// 是否显示左上角返回按钮
|
||||
final bool showBack;
|
||||
|
||||
/// 【新增】是否显示左上角菜单按钮(用于首页)
|
||||
final bool showMenuButton;
|
||||
|
||||
/// 返回按钮点击事件(默认 Get.back())
|
||||
final VoidCallback? onBackTap;
|
||||
|
||||
/// 页面主标题
|
||||
final String? title;
|
||||
|
||||
/// 页面副标题
|
||||
final String? subtitle;
|
||||
|
||||
/// 是否显示背景图
|
||||
final bool showBackgroundImage;
|
||||
|
||||
/// resizeToAvoidBottomInset
|
||||
final bool? resizeToAvoidBottomInset;
|
||||
|
||||
/// 为顶部内容(如返回按钮、标题)预留的高度
|
||||
final double topContentAreaHeight;
|
||||
|
||||
const HIBaseScaffold({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.showBack = true,
|
||||
this.showMenuButton = false,
|
||||
this.onBackTap,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.showBackgroundImage = true,
|
||||
this.topContentAreaHeight = 120.0,
|
||||
this.resizeToAvoidBottomInset = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 顶部状态栏高度
|
||||
final double topPadding = MediaQuery.of(context).padding.top + 8;
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
|
||||
backgroundColor: showBackgroundImage
|
||||
? Colors.transparent
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
toolbarHeight: 0.0,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 背景图全屏
|
||||
// if (showBackgroundImage)
|
||||
// const Positioned.fill(
|
||||
// child: KrLocalImage(
|
||||
// imageName: 'global-bg',
|
||||
// imageType: ImageType.jpg,
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// 页面主体内容(child)
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topContentAreaHeight),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
|
||||
// 左上角按钮区域
|
||||
Positioned(
|
||||
top: topPadding,
|
||||
left: 40.w,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// 优先显示菜单按钮
|
||||
if (showMenuButton) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.HI_MENU);
|
||||
},
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-home-stack',
|
||||
width: 48,
|
||||
height: 48,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 如果不显示菜单按钮,再根据 showBack 判断是否显示返回按钮
|
||||
else if (showBack) {
|
||||
return GestureDetector(
|
||||
onTap: onBackTap ?? () => Get.back(),
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: KrLocalImage(
|
||||
imageName: 'hi-back-icon',
|
||||
width: 48,
|
||||
height: 48,
|
||||
imageType: ImageType.svg,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 如果两者都不显示,则返回一个空容器
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 标题和副标题
|
||||
if (title != null || subtitle != null)
|
||||
Positioned(
|
||||
top: topPadding +
|
||||
(showBack ? 12.w : 20.w), // 顶部空出状态栏高度,并根据是否有返回按钮调整间距
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
// 使用 Column 来垂直排列标题
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // 水平居中
|
||||
children: [
|
||||
if (title != null)
|
||||
Text(
|
||||
title!,
|
||||
style: TextStyle(
|
||||
color: Colors.black, // 在深色背景下使用白色
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 0), // 副标题与主标题之间的小间距
|
||||
child: Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
color: Colors.black, // 副标题使用带透明度的白色
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w100,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// lib/app/widgets/hi_collapsible_list.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'dart:math' as math; // 导入 math 库以使用 pi
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../routes/app_pages.dart';
|
||||
|
||||
/// 可折叠列表项的数据模型
|
||||
class HICollapsibleItem {
|
||||
final String title;
|
||||
// 👇 核心改动 1: 将 content 类型从 String 改为 List<String>
|
||||
final List<String> content;
|
||||
|
||||
const HICollapsibleItem({required this.title, required this.content});
|
||||
}
|
||||
|
||||
/// 一个独立的、自定义样式的可折叠面板组件 (HI 前缀)
|
||||
///
|
||||
/// 它接收一个标题和一个内容字符串,并渲染成一个带边框、可展开/收起的面板。
|
||||
class HICollapsibleItemWidget extends StatefulWidget {
|
||||
const HICollapsibleItemWidget({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.initiallyExpanded = false,
|
||||
});
|
||||
|
||||
/// 要显示的数据项
|
||||
final HICollapsibleItem item;
|
||||
|
||||
/// 初始是否展开
|
||||
final bool initiallyExpanded;
|
||||
|
||||
@override
|
||||
State<HICollapsibleItemWidget> createState() => _HICollapsibleItemWidgetState();
|
||||
}
|
||||
|
||||
class _HICollapsibleItemWidgetState extends State<HICollapsibleItemWidget> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isExpanded = widget.initiallyExpanded;
|
||||
}
|
||||
|
||||
List<TextSpan> _buildTextSpans(String text) {
|
||||
final List<TextSpan> spans = [];
|
||||
final RegExp linkRegExp = RegExp(r'\[link\](.*?)\[/link\]'); // 正则表达式匹配 [link]...[/link]
|
||||
|
||||
text.splitMapJoin(
|
||||
linkRegExp,
|
||||
onMatch: (Match match) {
|
||||
final linkText = match.group(1)!; // 获取链接文本,例如 "点击这里"
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: linkText,
|
||||
style: const TextStyle(
|
||||
color: const Color(0xFFADFF5B), // 链接颜色
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
// 在这里处理点击事件,例如打开一个网页
|
||||
// 注意:您需要添加 url_launcher 依赖
|
||||
Get.toNamed(
|
||||
Routes.KR_WEBVIEW,
|
||||
arguments: {
|
||||
'url': 'https://www.baidu.com',
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String nonMatch) {
|
||||
spans.add(TextSpan(text: nonMatch)); // 普通文本部分
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final BorderRadius borderRadius = BorderRadius.circular(40.w);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 2.0),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ===== 标题部分 =====
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(24.w, 16.w, 24.w, 16.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.75 : 0.25,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: KrLocalImage(
|
||||
imageName: 'arrow-right-icon',
|
||||
imageType: ImageType.svg,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ===== 内容部分 =====
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: _isExpanded
|
||||
? Container(
|
||||
padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.item.content.map((itemText) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 8.h),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 11.w, right: 8.w),
|
||||
child: Container(
|
||||
width: 5.w,
|
||||
height: 5.w,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13.sp,
|
||||
height: 1.8,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
children: _buildTextSpans(itemText),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class HiFixedScrollbar extends StatefulWidget {
|
||||
final Widget child;
|
||||
final ScrollController controller;
|
||||
|
||||
final bool isShowScrollbar;
|
||||
|
||||
/// 距离右边的间距(默认 18)
|
||||
final double right;
|
||||
/// 滚动条宽度
|
||||
final double thickness;
|
||||
/// 滚动条颜色(默认白色 30%)
|
||||
final Color thumbColor;
|
||||
/// 背景轨道颜色(默认白色 15%)
|
||||
final Color trackColor;
|
||||
/// 滚动条固定高度(默认 50)
|
||||
final double thumbHeight;
|
||||
|
||||
const HiFixedScrollbar({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.controller,
|
||||
this.right = 18,
|
||||
this.isShowScrollbar = true,
|
||||
this.thickness = 5,
|
||||
this.thumbHeight = 50,
|
||||
this.thumbColor = const Color.fromRGBO(255, 255, 255, 0.3),
|
||||
this.trackColor = const Color.fromRGBO(255, 255, 255, 0.15),
|
||||
});
|
||||
|
||||
@override
|
||||
State<HiFixedScrollbar> createState() => _HiFixedScrollbarState();
|
||||
}
|
||||
|
||||
class _HiFixedScrollbarState extends State<HiFixedScrollbar> {
|
||||
double _thumbOffset = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_updateThumbPosition);
|
||||
}
|
||||
|
||||
void _updateThumbPosition() {
|
||||
if (!mounted) return;
|
||||
final position = widget.controller.position;
|
||||
if (!position.hasPixels || !position.hasContentDimensions) return;
|
||||
|
||||
final maxScrollExtent = position.maxScrollExtent;
|
||||
final offset = position.pixels;
|
||||
final viewport = position.viewportDimension;
|
||||
|
||||
// ✅ 固定高度滚动条,只根据滚动比例移动位置
|
||||
final trackHeight = viewport - widget.thumbHeight;
|
||||
final scrollRatio = maxScrollExtent == 0 ? 0 : offset / maxScrollExtent;
|
||||
|
||||
setState(() {
|
||||
_thumbOffset = (trackHeight * scrollRatio).clamp(0, trackHeight);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_updateThumbPosition);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
// 滚动内容
|
||||
widget.child,
|
||||
|
||||
if(widget.isShowScrollbar)
|
||||
...[
|
||||
// 滚动条轨道
|
||||
Positioned(
|
||||
right: widget.right.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: widget.thickness.w,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.trackColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 滚动条拇指
|
||||
Positioned(
|
||||
right: widget.right.w,
|
||||
top: _thumbOffset,
|
||||
child: Container(
|
||||
width: widget.thickness.w,
|
||||
height: widget.thumbHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.thumbColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// lib/app/widgets/hi_help_entrance.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
|
||||
/// 全局统一的帮助入口 Widget (HI 前缀)
|
||||
///
|
||||
/// 通常放置在 Stack 的顶层,通过 Positioned 控制位置。
|
||||
class HIHelpEntrance extends StatelessWidget {
|
||||
const HIHelpEntrance({
|
||||
super.key,
|
||||
this.bottom = 40.0,
|
||||
this.isLight = true,
|
||||
});
|
||||
|
||||
final double bottom;
|
||||
final bool isLight; // 用于控制主题颜色
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color displayColor = isLight ? Colors.white : Colors.black;
|
||||
|
||||
return Positioned(
|
||||
bottom: bottom,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.HI_HELP);
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
KrLocalImage(
|
||||
imageName: 'hi-home-logo',
|
||||
imageType: ImageType.svg,
|
||||
color: displayColor,
|
||||
width: 32.w,
|
||||
height: 32.w,
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
'遇到问题?',
|
||||
style: TextStyle(
|
||||
color: displayColor,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
height: 1.2,
|
||||
color: displayColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 简单的加载动画组件,替代有问题的flutter_spinkit
|
||||
/// ====================================================================
|
||||
/// 1. KRSimpleLoading: 旋转和扇形加载动画 (支持暂停,已修复重合)
|
||||
/// ====================================================================
|
||||
class KRSimpleLoading extends StatefulWidget {
|
||||
final Color? color;
|
||||
final double size;
|
||||
final Duration duration;
|
||||
final bool isPaused; // 控制动画暂停/恢复
|
||||
|
||||
const KRSimpleLoading({
|
||||
super.key,
|
||||
this.color,
|
||||
this.size = 40.0,
|
||||
this.size = 30.0, // 环的宽高默认为 30.0 像素
|
||||
this.duration = const Duration(milliseconds: 1000),
|
||||
this.isPaused = false, // 默认不暂停
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -20,23 +24,48 @@ class KRSimpleLoading extends StatefulWidget {
|
||||
class _KRSimpleLoadingState extends State<KRSimpleLoading>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
// 环的宽度 5px
|
||||
static const double _strokeWidth = 5.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
_animation = Tween<double>(
|
||||
|
||||
_rotationAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
curve: Curves.linear,
|
||||
));
|
||||
_controller.repeat();
|
||||
|
||||
// 根据初始状态决定是重复播放还是停止
|
||||
_setAnimationState(widget.isPaused);
|
||||
}
|
||||
|
||||
void _setAnimationState(bool isPaused) {
|
||||
if (isPaused) {
|
||||
_controller.stop();
|
||||
} else {
|
||||
// 如果之前停止了,调用 repeat 重新开始
|
||||
_controller.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant KRSimpleLoading oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// 监听 isPaused 参数的变化,并更新动画状态
|
||||
if (widget.isPaused != oldWidget.isPaused) {
|
||||
_setAnimationState(widget.isPaused);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -47,38 +76,58 @@ class _KRSimpleLoadingState extends State<KRSimpleLoading>
|
||||
|
||||
@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,
|
||||
final defaultColor = widget.color ?? Theme.of(context).primaryColor;
|
||||
|
||||
// 强制动画组件拥有固定的 30x30 尺寸
|
||||
return SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _rotationAnimation.value * 2 * 3.14159,
|
||||
// 关键修复: 使用 Stack 和 Center+SizedBox 确保两个环完美重合
|
||||
child: Stack(
|
||||
children: [
|
||||
// 1. 底环 (Container): 绘制 30x30 边界内的 5px 边框
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: defaultColor.withOpacity(0.3),
|
||||
width: _strokeWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 2. 顶扇形 (CircularProgressIndicator):
|
||||
// 通过 Center+SizedBox 强制其绘制区域收缩,以消除渲染偏差。
|
||||
Center(
|
||||
child: SizedBox(
|
||||
// 关键:将尺寸减去笔触宽度 (30 - 5 = 25)
|
||||
width: widget.size - _strokeWidth,
|
||||
height: widget.size - _strokeWidth,
|
||||
child: CircularProgressIndicator(
|
||||
value: 0.2, // 彩色扇形长度
|
||||
strokeCap: StrokeCap.round, // 圆角
|
||||
strokeWidth: _strokeWidth, // 宽度 5px
|
||||
backgroundColor: Colors.transparent, // 必须透明
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
defaultColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 脉冲加载动画
|
||||
class KRSimplePulse extends StatefulWidget {
|
||||
final Color? color;
|
||||
final double size;
|
||||
|
||||
@@ -100,7 +100,7 @@ class _ToastWidget extends StatelessWidget {
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 15.0,
|
||||
color: Colors.white,
|
||||
color: const Color(0xFFADFF5B),
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user