This commit is contained in:
wyf
2025-05-13 11:59:04 +08:00
parent eae7a2284d
commit fb5c3864a3
101 changed files with 8427 additions and 1953 deletions

View File

@@ -43,7 +43,7 @@ class _TestWidgetState extends State<NullDataWidget> {
child: SvgPicture.asset(
'assets/img/icon/nulldata.svg',
fit: BoxFit.cover,
color: themeController.currentColor.sc4,
color: themeController.currentColor.sc4.withOpacity(0.5),
),
),
),

View File

@@ -0,0 +1,264 @@
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/color/appConstants.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
import 'package:vbvs_app/common/util/MyUtils.dart'; // 假设你自己扩展的 .rpx
import 'SleepdateWidget.dart'; // 导入你自定义的 SleepdateWidget
class SleepCalendarWidget extends StatefulWidget {
final int? timestamp; // 可选时间戳
const SleepCalendarWidget({super.key, this.timestamp});
@override
State<SleepCalendarWidget> createState() => _SleepCalendarWidgetState();
}
class _SleepCalendarWidgetState extends State<SleepCalendarWidget> {
late DateTime _currentDate;
@override
void initState() {
super.initState();
_currentDate = widget.timestamp != null
? DateTime.fromMillisecondsSinceEpoch(widget.timestamp!)
: DateTime.now();
}
List<DateTime> getDaysInMonth(DateTime date) {
// 获取当前月份的第一天
DateTime firstDayOfMonth = DateTime(date.year, date.month, 1);
// 获取当前月份的最后一天
DateTime lastDayOfMonth = DateTime(date.year, date.month + 1, 0);
List<DateTime> days = [];
// 获取该月的所有日期
for (int i = 0; i < lastDayOfMonth.day; i++) {
days.add(firstDayOfMonth.add(Duration(days: i)));
}
return days;
}
List<List<DateTime>> getCalendarRows(List<DateTime> daysInMonth) {
// 获取该月所有日期后,处理为 7 列的格式
List<List<DateTime>> calendarRows = [];
int firstWeekday = daysInMonth.first.weekday; // 获取该月第一天是周几
int emptyDays = (firstWeekday == 7 ? 0 : firstWeekday) - 1; // 调整为空白天数
List<DateTime> row = [];
for (int i = 0; i < emptyDays; i++) {
row.add(DateTime(0)); // 填充空白日期
}
for (var day in daysInMonth) {
row.add(day);
if (row.length == 7) {
// 如果当前行满了 7 个日期,则添加到 calendarRows 中,并重置 row
calendarRows.add(List.from(row));
row.clear();
}
}
if (row.isNotEmpty) {
// 如果最后一行的日期不足 7 个,则补充空白日期
while (row.length < 7) {
row.add(DateTime(0)); // 填充空白日期
}
calendarRows.add(List.from(row)); // 添加最后一行
}
return calendarRows;
}
@override
Widget build(BuildContext context) {
List<Map<String, dynamic>> showLabel = [
{
"level": 1,
"name": "优秀",
"color": Color(0xFF4CAF50), // 绿色
},
{
"level": 2,
"name": "良好",
"color": Color(0xFF8BC34A), // 浅绿
},
{
"level": 3,
"name": "合格",
"color": Color(0xFFFFC107), // 黄色
},
{
"level": 4,
"name": "注意",
"color": Color(0xFFF44336), // 红色
},
{
"level": 5,
"name": "无报告",
"color": Color(0xFF9E9E9E), // 灰色
},
];
List sleepData = [];
// 获取当前月的所有日期
List<DateTime> daysInMonth = getDaysInMonth(_currentDate);
// 获取按行排列的日期
List<List<DateTime>> calendarRows = getCalendarRows(daysInMonth);
return Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
topLeft: Radius.circular(20.rpx),
topRight: Radius.circular(20.rpx),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
// height: MediaQuery.sizeOf(context).height * 0.055,
constraints: BoxConstraints(
minHeight: 90.rpx,
),
decoration: BoxDecoration(
color: const Color(0xFF313541),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
topLeft: Radius.circular(20.rpx),
topRight: Radius.circular(20.rpx),
),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
65.rpx,
0.rpx,
65.rpx,
0.rpx,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.arrow_back_ios_new,
color: const Color(0xFF6D6F73),
size: 24.rpx,
),
Text(
'${_currentDate.year}${_currentDate.month}',
style: TextStyle(
color: Colors.white,
fontSize: 30.rpx,
letterSpacing: 0.0,
),
),
Icon(
Icons.arrow_forward_ios,
color: const Color(0xFF6D6F73),
size: 24.rpx,
),
],
),
),
),
Container(
width: double.infinity,
constraints: BoxConstraints(
minHeight: 720.rpx,
),
decoration: const BoxDecoration(
color: Color(0xFF242835),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
65.rpx,
13.rpx,
65.rpx,
38.rpx,
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
// Weekdays Header
Container(
constraints: BoxConstraints(minHeight: 90.rpx),
child: Row(
children: [
for (var day in ["", "", "", "", "", "", ""])
Expanded(
child: Center(
child: Text(
day,
style: TextStyle(
color: stringToColor("#FFFFFF"),
fontSize: AppConstants().normal_text_fontSize,
),
),
),
),
],
),
),
// 日历显示部分
Column(
children: calendarRows.map((week) {
return Row(
mainAxisAlignment: MainAxisAlignment.start, // 保证每一行左对齐
children: week.map((date) {
return Expanded(
child: Padding(
padding: EdgeInsets.all(4.rpx),
child: date.year != 0 // 如果是空白日期就不显示
? SleepdateWidget(date: date)
: SizedBox.shrink(),
),
);
}).toList(),
);
}).toList(),
),
SizedBox(
height: 55.rpx,
),
Wrap(
direction: Axis.horizontal, // 默认是水平排列的,可以去掉这行
spacing: 20.rpx, // 水平间距
runSpacing: 20.rpx, // 垂直间距
children: showLabel.map<Widget>((item) {
return Container(
padding: EdgeInsets.all(5.rpx), // 可选,添加一点间距
child: Row(
mainAxisSize: MainAxisSize.min, // 确保 Row 不会占满整个宽度
children: [
Container(
width: 20.rpx,
height: 20.rpx,
decoration: BoxDecoration(
color: item["color"],
borderRadius:
BorderRadius.circular(10.rpx), // 圆形效果
),
),
SizedBox(width: 8.rpx), // 标签和文本之间的间距
Text(
item["name"],
style: TextStyle(
color: Colors.white,
fontSize: 24.rpx,
),
),
],
),
);
}).toList(),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:vbvs_app/common/util/FitTool.dart';
class SleepdateWidget extends StatelessWidget {
final DateTime date;
const SleepdateWidget({super.key, required this.date});
@override
Widget build(BuildContext context) {
return Container(
width: 90.rpx,
height: 90.rpx,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.rpx),
),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(10.rpx, 10.rpx, 10.rpx, 10.rpx),
child: Container(
decoration: BoxDecoration(
color: Color(0xFFDC1C1C), // 默认红色
shape: BoxShape.circle,
),
child: Align(
alignment: AlignmentDirectional(0, 0),
child: Text(
'${date.day}',
style: TextStyle(
color: Colors.white,
fontSize: 26.rpx,
letterSpacing: 0.0,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,461 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutterflow_ui/flutterflow_ui.dart';
/// A dropdown widget that allows the user to select an option from a list of options.
class THFlutterFlowDropDown<T> extends StatefulWidget {
const THFlutterFlowDropDown({
super.key,
this.controller,
this.multiSelectController,
this.hintText,
this.searchHintText,
required this.options,
this.optionLabels,
this.onChanged,
this.onMenuStateChange,
this.onMultiSelectChanged,
this.icon,
this.width,
this.height,
this.maxHeight,
this.fillColor,
this.searchHintTextStyle,
this.hintTextStyle,
this.searchTextStyle,
this.searchCursorColor,
required this.textStyle,
required this.elevation,
required this.borderWidth,
required this.borderRadius,
required this.borderColor,
required this.margin,
this.hidesUnderline = false,
this.disabled = false,
this.isOverButton = false,
this.menuOffset,
this.isSearchable = false,
this.isMultiSelect = false,
this.labelText,
this.labelTextStyle,
this.allowEmpty = true, // 新增参数,默认值为 true
}) : assert(
isMultiSelect
? (controller == null &&
onChanged == null &&
multiSelectController != null &&
onMultiSelectChanged != null)
: (controller != null &&
onChanged != null &&
multiSelectController == null &&
onMultiSelectChanged == null),
);
/// The controller for the dropdown field.
final FormFieldController<T?>? controller;
/// The controller for the multi-select dropdown field.
final FormFieldController<List<T>?>? multiSelectController;
/// The text to display as a hint when no option is selected.
final String? hintText;
/// The text to display as a hint in the search field.
final String? searchHintText;
/// The list of options to display in the dropdown.
final List<T> options;
/// The list of labels corresponding to the options.
final List<String>? optionLabels;
/// A callback function that is called when the selected option changes.
final Function(T?)? onChanged;
final Function(bool)? onMenuStateChange;
/// A callback function that is called when the selected options change in multi-select mode.
final Function(List<T>?)? onMultiSelectChanged;
/// The icon to display in the dropdown field.
final Widget? icon;
/// The width of the dropdown field.
final double? width;
/// The height of the dropdown field.
final double? height;
/// The maximum height of the dropdown menu.
final double? maxHeight;
/// The background color of the dropdown field.
final Color? fillColor;
/// The text style for the search hint text.
final TextStyle? searchHintTextStyle;
final TextStyle? hintTextStyle;
/// The text style for the search text.
final TextStyle? searchTextStyle;
/// The color of the search cursor.
final Color? searchCursorColor;
/// The text style for the dropdown field.
final TextStyle textStyle;
/// The elevation of the dropdown menu.
final double elevation;
/// The width of the dropdown field's border.
final double borderWidth;
/// The border radius of the dropdown field.
final double borderRadius;
/// The color of the dropdown field's border.
final Color borderColor;
/// The margin around the dropdown field.
final EdgeInsetsGeometry margin;
/// Whether to hide the underline of the dropdown field.
final bool hidesUnderline;
/// Whether the dropdown is disabled.
final bool disabled;
/// Whether the dropdown menu is displayed over the button.
final bool isOverButton;
/// The offset of the dropdown menu.
final Offset? menuOffset;
/// Whether the dropdown is searchable.
final bool isSearchable;
/// Whether the dropdown is in multi-select mode.
final bool isMultiSelect;
/// The label text for the dropdown field.
final String? labelText;
/// The text style for the label text.
final TextStyle? labelTextStyle;
/// Whether the dropdown allows empty selection.
final bool allowEmpty;
@override
State<THFlutterFlowDropDown<T>> createState() =>
_FlutterFlowDropDownState<T>();
}
class _FlutterFlowDropDownState<T> extends State<THFlutterFlowDropDown<T>> {
bool get isMultiSelect => widget.isMultiSelect;
FormFieldController<T?> get controller => widget.controller!;
FormFieldController<List<T>?> get multiSelectController =>
widget.multiSelectController!;
T? get currentValue {
final value = isMultiSelect
? multiSelectController.value?.firstOrNull
: controller.value;
return widget.options.contains(value) ? value : null;
}
Set<T> get currentValues {
if (!isMultiSelect || multiSelectController.value == null) {
return {};
}
return widget.options
.toSet()
.intersection(multiSelectController.value!.toSet());
}
Map<T, String> get optionLabels => Map.fromEntries(
widget.options.asMap().entries.map(
(option) => MapEntry(
option.value,
widget.optionLabels == null ||
widget.optionLabels!.length < option.key + 1
? option.value.toString()
: widget.optionLabels![option.key],
),
),
);
EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp(
EdgeInsetsDirectional.zero,
const EdgeInsetsDirectional.symmetric(horizontal: double.infinity),
);
late void Function() _listener;
final TextEditingController _textEditingController = TextEditingController();
List<T>? _previousSelectedValues;
@override
void initState() {
super.initState();
if (isMultiSelect) {
_listener = () {
if (!widget.allowEmpty &&
(multiSelectController.value == null ||
multiSelectController.value!.isEmpty) &&
(widget.options != null && widget.options.isNotEmpty)) {
// showToast("请至少选择一项", color: color_warning);
multiSelectController.value = _previousSelectedValues;
} else {
_previousSelectedValues = List.from(multiSelectController.value!);
}
widget.onMultiSelectChanged!(multiSelectController.value);
};
multiSelectController.addListener(_listener);
} else {
_listener = () {
setState(() {});
widget.onChanged!(controller.value);
};
controller.addListener(_listener);
}
}
@override
void dispose() {
if (isMultiSelect) {
multiSelectController.removeListener(_listener);
} else {
controller.removeListener(_listener);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final dropdownWidget = _buildDropdownWidget();
return SizedBox(
width: widget.width,
height: widget.height,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(
color: widget.borderColor,
width: widget.borderWidth,
),
color: widget.fillColor,
),
child: Padding(
padding: _useDropdown2() ? EdgeInsets.zero : widget.margin,
child: widget.hidesUnderline
? DropdownButtonHideUnderline(child: dropdownWidget)
: dropdownWidget,
),
),
);
}
bool _useDropdown2() =>
widget.isMultiSelect ||
widget.isSearchable ||
!widget.isOverButton ||
widget.maxHeight != null;
Widget _buildDropdownWidget() =>
_useDropdown2() ? _buildDropdown() : _buildLegacyDropdown();
Widget _buildLegacyDropdown() {
return DropdownButtonFormField<T>(
value: currentValue,
hint: _createHintText(),
items: _createMenuItems(),
elevation: widget.elevation.toInt(),
onChanged: widget.disabled ? null : (value) => controller.value = value,
icon: widget.icon,
isExpanded: true,
dropdownColor: widget.fillColor,
focusColor: Colors.transparent,
decoration: InputDecoration(
labelText: widget.labelText == null || widget.labelText!.isEmpty
? null
: widget.labelText,
labelStyle: widget.labelTextStyle,
border: widget.hidesUnderline
? InputBorder.none
: const UnderlineInputBorder(),
),
);
}
Text? _createHintText() => widget.hintText != null
? Text(
widget.hintText!,
style: widget.hintTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis, // 超出部分显示省略号
)
: null;
List<DropdownMenuItem<T>> _createMenuItems() => widget.options
.map(
(option) => DropdownMenuItem<T>(
value: option,
child: Padding(
padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero,
child: Text(optionLabels[option] ?? '', style: widget.textStyle),
),
),
)
.toList();
List<DropdownMenuItem<T>> _createMultiselectMenuItems() => widget.options
.map(
(item) => DropdownMenuItem<T>(
value: item,
// Disable default onTap to avoid closing menu when selecting an item
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final isSelected =
multiSelectController.value?.contains(item) ?? false;
return InkWell(
onTap: () {
multiSelectController.value ??= [];
isSelected
? multiSelectController.value!.remove(item)
: multiSelectController.value!.add(item);
multiSelectController.update();
// This rebuilds the StatefulWidget to update the button's text.
setState(() {});
// This rebuilds the dropdownMenu Widget to update the check mark.
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: horizontalMargin,
child: Row(
children: [
if (isSelected)
const Icon(Icons.check_box_outlined)
else
const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Expanded(
child: Text(
optionLabels[item]!,
style: widget.textStyle,
),
),
],
),
),
);
},
),
),
)
.toList();
Widget _buildDropdown() {
final overlayColor = WidgetStateProperty.resolveWith<Color?>((states) =>
states.contains(WidgetState.focused) ? Colors.transparent : null);
final iconStyleData = widget.icon != null
? IconStyleData(icon: widget.icon!)
: const IconStyleData();
return DropdownButton2<T>(
value: currentValue,
hint: _createHintText(),
items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(),
iconStyleData: iconStyleData,
buttonStyleData: ButtonStyleData(
elevation: widget.elevation.toInt(),
overlayColor: WidgetStateProperty.all(Colors.transparent),
padding: widget.margin,
),
menuItemStyleData: MenuItemStyleData(
overlayColor: WidgetStateProperty.all(Colors.transparent),
padding: EdgeInsets.zero,
),
dropdownStyleData: DropdownStyleData(
elevation: widget.elevation.toInt(),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
color: widget.fillColor,
),
isOverButton: widget.isOverButton,
offset: widget.menuOffset ?? Offset.zero,
maxHeight: widget.maxHeight,
padding: EdgeInsets.zero,
),
onChanged: widget.disabled
? null
: (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val),
isExpanded: true,
selectedItemBuilder: (context) => widget.options
.map(
(item) => Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
isMultiSelect
? currentValues
.where((v) => optionLabels.containsKey(v))
.map((v) => optionLabels[v])
.join(', ')
: optionLabels[item]!,
style: widget.textStyle,
maxLines: 1,
),
),
)
.toList(),
dropdownSearchData: widget.isSearchable
? DropdownSearchData<T>(
searchController: _textEditingController,
searchInnerWidgetHeight: 50,
searchInnerWidget: Container(
height: 50,
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
expands: true,
maxLines: null,
controller: _textEditingController,
cursorColor: widget.searchCursorColor,
style: widget.searchTextStyle,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: widget.searchHintText,
hintStyle: widget.searchHintTextStyle,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
searchMatchFn: (item, searchValue) {
return (optionLabels[item.value] ?? '')
.toLowerCase()
.contains(searchValue.toLowerCase());
},
)
: null,
// This is to clear the search value when you close the menu
onMenuStateChange: (isOpen) {
if (widget.isSearchable && !isOpen) {
_textEditingController.clear();
}
if (widget.onMenuStateChange != null) {
widget.onMenuStateChange!(isOpen);
}
},
);
}
}

View File

@@ -149,7 +149,7 @@ class DynamicReportDetailWidget extends StatelessWidget {
style: FlutterFlowTheme.of(Get.context!).bodyMedium.override(
fontFamily: 'Inter',
fontSize: 28.rpx,
color: themeController.currentColor.sc3,
color: themeController.currentColor.sc4,
),
),
);

View File

@@ -44,7 +44,8 @@ class _SleepDataModuleWidgetState extends State<SleepDataModuleWidget> {
print('点击了离床次数卡片');
},
child: Container(
width: MediaQuery.sizeOf(context).width * 0.27,
// width: MediaQuery.sizeOf(context).width * 0.267,
width: MediaQuery.sizeOf(context).width * 0.267,
constraints: BoxConstraints(
minWidth: 200.rpx,
minHeight: 161.rpx,

View File

@@ -42,8 +42,9 @@ class _SleepDateWidgetState extends State<SleepDateWidget> {
String day = MyUtils.formatDateTimeDay(widget.date);
// 选中时背景色为黑色,否则为透明
Color backgroundColor =
widget.isSelected == true ? Colors.black : Colors.transparent;
Color backgroundColor = widget.isSelected == true
? Colors.black.withOpacity(0.3)
: Colors.transparent;
return ClickableContainer(
backgroundColor: backgroundColor,

View File

@@ -20,23 +20,20 @@ class ClickableContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(splashFactory: InkRipple.splashFactory),
child: Material(
color: Colors.transparent,
child: Ink(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
),
child: InkWell(
borderRadius: BorderRadius.circular(borderRadius),
onTap: onTap,
splashColor: highlightColor.withOpacity(0.2),
child: Padding(
padding: padding,
child: child,
),
return Material(
color: Colors.transparent,
child: Ink(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
),
child: InkWell(
borderRadius: BorderRadius.circular(borderRadius),
onTap: onTap,
splashColor: highlightColor.withOpacity(0.5),
child: Padding(
padding: padding,
child: child,
),
),
),

View File

@@ -21,10 +21,151 @@ class TopSlideNotification extends StatefulWidget {
this.duration = const Duration(seconds: 2),
});
static OverlayEntry? _currentEntry; // 单例 OverlayEntry
static bool _isShowing = false;
static void show(
BuildContext context, {
String text = '操作成功!',
double fontSize = 16,
Color? textColor,
double slideOffset = 300.0,
Duration duration = const Duration(seconds: 2),
}) {
// 如果已有弹窗,先移除
_removeCurrentEntry();
final overlay = Overlay.of(context);
final entry = OverlayEntry(
builder: (_) => TopSlideNotification(
text: text,
fontSize: fontSize,
textColor: textColor,
slideOffset: slideOffset,
duration: duration,
),
);
_currentEntry = entry;
_isShowing = true;
overlay.insert(entry);
// 自动移除
Future.delayed(duration + const Duration(milliseconds: 500), () {
_removeCurrentEntry();
});
}
static void _removeCurrentEntry() {
if (_currentEntry != null && _isShowing) {
_currentEntry!.remove();
_currentEntry = null;
_isShowing = false;
}
}
@override
State<TopSlideNotification> createState() => _TopSlideNotificationState();
}
/// 工具方法:调用时直接加进 Overlay 上
class _TopSlideNotificationState extends State<TopSlideNotification>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;
bool _isAnimating = false; // 标志位,控制是否正在动画中
@override
void initState() {
super.initState();
// 初始化 AnimationController
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
// 动画初始化完成后调用
SchedulerBinding.instance.addPostFrameCallback((_) {
_startAnimation(); // 调用动画启动方法
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 获取屏幕高度,用于计算动画的偏移值
final screenHeight = MediaQuery.of(context).size.height;
final offsetValue = widget.slideOffset! / screenHeight;
// 设置动画
_animation =
Tween<Offset>(begin: const Offset(0, -1), end: Offset(0, offsetValue))
.animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
));
}
@override
void dispose() {
// 确保动画控制器在组件销毁时被释放
_controller.dispose();
super.dispose();
}
Color get _textColor {
return widget.textColor ?? Get.find<ThemeController>().currentColor.sc2;
}
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SlideTransition(
position: _animation,
child: Material(
color: stringToColor("#000000").withOpacity(0.8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Container(
child: Text(
widget.text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: widget.fontSize,
color: _textColor,
),
),
),
),
),
),
);
}
// 执行动画
Future<void> _startAnimation() async {
if (_isAnimating) return; // 如果正在动画中,则不执行新的动画
_isAnimating = true; // 标记动画开始
try {
await _controller.forward();
await Future.delayed(widget.duration);
// 只有在组件仍然挂载时才执行 reverse 动作
if (mounted) {
await _controller.reverse();
}
} finally {
_isAnimating = false; // 动画完成后,标记动画结束
}
}
// 工具方法:调用时直接加进 Overlay 上
static void show(
BuildContext context, {
String text = '操作成功!',
@@ -50,81 +191,3 @@ class TopSlideNotification extends StatefulWidget {
});
}
}
class _TopSlideNotificationState extends State<TopSlideNotification>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
SchedulerBinding.instance.addPostFrameCallback((_) async {
await _controller.forward();
await Future.delayed(widget.duration);
await _controller.reverse();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final screenHeight = MediaQuery.of(context).size.height;
final offsetValue = widget.slideOffset! / screenHeight;
_animation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset(0, offsetValue),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Color get _textColor {
return widget.textColor ?? Get.find<ThemeController>().currentColor.sc2;
}
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SlideTransition(
position: _animation,
child: Material(
color: stringToColor("#000000").withOpacity(0.8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Container(
// color: Colors.red,
child: Text(
widget.text,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: widget.fontSize,
color: _textColor,
),
),
),
),
),
),
);
}
}

View File

@@ -1,51 +1,79 @@
// import 'package:flutter/material.dart';
// import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
// class WebViewWidget extends StatefulWidget {
// final String url;
// const WebViewWidget({Key? key, required this.url}) : super(key: key);
class MyWebView extends StatefulWidget {
final String url;
final Function()? onLoad;
final Function(MyWebView view, String msg)? onMessage;
// @override
// _WebViewWidgetState createState() => _WebViewWidgetState();
// }
const MyWebView({
Key? key,
required this.url,
this.onLoad,
this.onMessage,
}) : super(key: key);
// class _WebViewWidgetState extends State<WebViewWidget> {
// late WebViewController _webViewController;
@override
State<MyWebView> createState() => _MyWebViewState();
}
// @override
// void initState() {
// super.initState();
// // 初始化 WebView 控件
// WebView.platform = SurfaceAndroidWebView();
// }
class _MyWebViewState extends State<MyWebView> {
late final WebViewController _controller;
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// title: Text('WebView'),
// ),
// body: WebView(
// initialUrl: widget.url, // 设置要打开的网页地址
// javascriptMode: JavascriptMode.unrestricted, // 启用 JavaScript
// onWebViewCreated: (WebViewController webViewController) {
// _webViewController = webViewController;
// },
// onPageStarted: (String url) {
// print("页面开始加载:$url");
// },
// onPageFinished: (String url) {
// print("页面加载完成:$url");
// },
// navigationDelegate: (NavigationRequest request) {
// if (request.url.startsWith('https://www.google.com/')) {
// print('拦截了URL请求: ${request.url}');
// return NavigationDecision.prevent; // 拦截特定的请求
// }
// return NavigationDecision.navigate;
// },
// gestureNavigationEnabled: true, // 启用手势返回
// ),
// );
// }
// }
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
widget.onLoad?.call();
},
onWebResourceError: (error) {
print("WebView 加载错误: ${error.description}");
},
onNavigationRequest: (NavigationRequest request) {
final url = request.url;
if (url.startsWith('http') || url.startsWith('https')) {
return NavigationDecision.navigate;
}
if (url.startsWith('weixin://')) {
_launchWeChatUrl(url);
return NavigationDecision.prevent;
}
print('拦截未知协议: $url');
return NavigationDecision.prevent;
},
),
)
..addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (msg) {
widget.onMessage?.call(widget, msg.message);
},
)
..loadRequest(Uri.parse(widget.url));
}
void _launchWeChatUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
print('⚠️ 无法跳转微信: $url');
}
}
// 提供方法给外部调用 JS
void sendData(String data) {
_controller.runJavaScript("window.postMessage('$data')");
}
@override
Widget build(BuildContext context) {
return WebViewWidget(controller: _controller);
}
}