更新
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
264
lib/component/base/SleepCalendarWidget.dart
Normal file
264
lib/component/base/SleepCalendarWidget.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/component/base/SleepdateWidget.dart
Normal file
39
lib/component/base/SleepdateWidget.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
461
lib/component/base/THFlutterFlowDropDown.dart
Normal file
461
lib/component/base/THFlutterFlowDropDown.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user