484 lines
14 KiB
Dart
484 lines
14 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:cashier_reserve/common/base/ui.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
|
|
/// 应用主体背景颜色
|
|
const Color appBackGroundColor = Color.fromARGB(255, 247, 250, 255);
|
|
|
|
/// 主色调
|
|
const Color mdMainColor = Color(0xff2854BC);
|
|
|
|
/// fix for Flutter 2.5
|
|
/// https://github.com/flutter/flutter/issues/77904
|
|
Brightness? getAppBrightness(bool isLight) {
|
|
if (kIsWeb) {
|
|
return null;
|
|
}
|
|
if (Platform.isIOS) {
|
|
return isLight ? Brightness.dark : Brightness.light;
|
|
}
|
|
return isLight ? Brightness.light : Brightness.dark;
|
|
}
|
|
|
|
/// 微信风格Appbar
|
|
AppBar makeWechatStyleAppbar(BuildContext context, String title,
|
|
{List<Widget>? actions,
|
|
final Color backGroundColor = Colors.white,
|
|
final Color textColor = Colors.black,
|
|
final Brightness brightness = Brightness.light,
|
|
bool centerTitle = false,
|
|
PreferredSizeWidget? bottom}) {
|
|
return AppBar(
|
|
backgroundColor: backGroundColor,
|
|
elevation: 0,
|
|
centerTitle: centerTitle,
|
|
systemOverlayStyle: SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: getAppBrightness(false),
|
|
statusBarBrightness: getAppBrightness(false)),
|
|
actions: actions,
|
|
bottom: bottom,
|
|
title: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 17, color: textColor, fontWeight: FontWeight.bold),
|
|
),
|
|
leading: IconButton(
|
|
iconSize: 18,
|
|
tooltip: "返回",
|
|
color: Colors.black,
|
|
icon: Icon(
|
|
Icons.arrow_back_ios,
|
|
color: textColor,
|
|
),
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 通用Appbar
|
|
AppBar makeAppbar(BuildContext context, String? title,
|
|
{List<Widget>? actions,
|
|
Color? backgroundColor,
|
|
PreferredSizeWidget? bottom,
|
|
final bool showBack = true,
|
|
Widget? titleWidget,
|
|
Widget? backWidget,
|
|
VoidCallback? onBack,
|
|
Key? key}) {
|
|
backgroundColor ??= mdMainColor;
|
|
return AppBar(
|
|
key: key,
|
|
backgroundColor: backgroundColor,
|
|
elevation: 0,
|
|
centerTitle: true,
|
|
actions: actions,
|
|
bottom: bottom,
|
|
systemOverlayStyle: SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: getAppBrightness(false),
|
|
statusBarBrightness: getAppBrightness(false)),
|
|
title: titleWidget ??
|
|
Text(
|
|
title!,
|
|
style: const TextStyle(
|
|
fontSize: 17, color: Colors.white, fontWeight: FontWeight.bold),
|
|
),
|
|
leading: showBack
|
|
? (backWidget ??
|
|
IconButton(
|
|
iconSize: 18,
|
|
tooltip: "返回",
|
|
color: Colors.white,
|
|
splashColor: Colors.black,
|
|
hoverColor: Colors.black,
|
|
icon: const Icon(Icons.arrow_back_ios),
|
|
onPressed: onBack ??
|
|
() {
|
|
Navigator.pop(context);
|
|
},
|
|
))
|
|
: null,
|
|
);
|
|
}
|
|
|
|
/// ListView 无感上拉加载
|
|
class ListViewPullUpLoadMore {
|
|
final ScrollController scrollController;
|
|
final Future<void> Function() onLoadMore;
|
|
|
|
bool _isLoading = false;
|
|
|
|
ListViewPullUpLoadMore(this.scrollController, {required this.onLoadMore}) {
|
|
scrollController.addListener(() {
|
|
if (_isLoading) {
|
|
return;
|
|
}
|
|
if (scrollController.offset ==
|
|
scrollController.position.maxScrollExtent) {
|
|
/// 到底部,添加加载中标记,触发回调
|
|
_isLoading = true;
|
|
onLoadMore().then((_) {
|
|
/// 加载完毕,标记复位
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 绘制通用下划线
|
|
Widget makeDivider(BuildContext? context,
|
|
{Color? color, EdgeInsetsGeometry? padding, double? height}) {
|
|
color ??= Colors.black.withAlpha(50);
|
|
return Padding(
|
|
padding: padding ??
|
|
const EdgeInsets.only(left: 18.5, right: 18.5, top: 5, bottom: 5),
|
|
child: Divider(
|
|
color: color,
|
|
height: height ?? 0.5,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 卡片容器
|
|
Widget makeCardContainer(
|
|
{required BuildContext context,
|
|
required Widget child,
|
|
Key? key,
|
|
EdgeInsetsGeometry? padding,
|
|
EdgeInsetsGeometry? paddingIn,
|
|
BorderRadius? borderRadius,
|
|
Color? color,
|
|
double? elevation,
|
|
double? width,
|
|
bool nullWidth = false}) {
|
|
return SizedBox(
|
|
key: key,
|
|
width: nullWidth ? null : (width ?? MediaQuery.of(context).size.width),
|
|
child: Padding(
|
|
padding: padding ?? const EdgeInsets.only(left: 8, right: 8),
|
|
child: Card(
|
|
color: color,
|
|
elevation: elevation ?? 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: borderRadius ?? BorderRadius.circular(0),
|
|
),
|
|
child: Padding(
|
|
padding:
|
|
paddingIn ?? const EdgeInsets.only(top: 10, bottom: 10),
|
|
child: child,
|
|
))));
|
|
}
|
|
|
|
/// 通用页面加载动画
|
|
Widget makeLoadBody(BuildContext context) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
|
|
/// 通用网络图片
|
|
Widget makeNetImage(BuildContext context, String? imageUrl,
|
|
{BoxFit? fit, double? height, double? width}) {
|
|
imageUrl ??= "https://image.xiaomfzs.com/xmf-app/user_icon.png";
|
|
|
|
if (kIsWeb) {
|
|
return Image.network(imageUrl, fit: fit, height: height, width: width);
|
|
}
|
|
|
|
return CachedNetworkImage(
|
|
height: height,
|
|
width: width,
|
|
imageUrl: imageUrl,
|
|
fit: fit,
|
|
placeholder: (context, url) {
|
|
return Container(
|
|
color: Colors.black.withAlpha(50),
|
|
child: Center(
|
|
child: Image.asset("images/loading.png"),
|
|
));
|
|
},
|
|
errorWidget: (BuildContext context, String url, dynamic error) {
|
|
return const Icon(Icons.error);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 通用头像
|
|
Widget makeUserAvatar(BuildContext context, String? imgUrl,
|
|
{final double size = 46}) {
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10000.0),
|
|
child: makeNetImage(context, imgUrl, fit: BoxFit.fill),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 底部按钮边距容器
|
|
Widget makeNavButtonPadding(Widget? widget) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 14),
|
|
child: widget,
|
|
);
|
|
}
|
|
|
|
/// 底部按钮
|
|
Widget makeBottomNavButton(String text,
|
|
{required VoidCallback? onTap,
|
|
Color color = mdMainColor,
|
|
EdgeInsetsGeometry? padding,
|
|
TextStyle? textStyle,
|
|
double? elevation}) {
|
|
return Padding(
|
|
padding: padding ?? const EdgeInsets.only(left: 20, right: 20),
|
|
child: ElevatedButton(
|
|
style: TextButton.styleFrom(
|
|
backgroundColor: color, elevation: elevation ?? 1),
|
|
onPressed: onTap,
|
|
child: Text(
|
|
text,
|
|
style: textStyle ??
|
|
const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 收起键盘容器
|
|
Widget makeDismissKeyboardContainer(BuildContext context, Widget child,
|
|
{VoidCallback? onDismissed}) {
|
|
return GestureDetector(
|
|
onTap: () {
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
if (onDismissed != null) {
|
|
onDismissed();
|
|
}
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
/// 底部弹出按钮
|
|
Future<String?> showBottomMenu(
|
|
BuildContext context, Map<String, String> items) async {
|
|
return await showCupertinoModalPopup<String?>(
|
|
context: context,
|
|
builder: (ctx) {
|
|
return CupertinoActionSheet(
|
|
cancelButton: CupertinoActionSheetAction(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text(
|
|
'取消',
|
|
style: TextStyle(fontSize: 14, color: Colors.red),
|
|
),
|
|
),
|
|
actions: <Widget>[
|
|
for (var kv in (items.entries))
|
|
CupertinoActionSheetAction(
|
|
onPressed: () {
|
|
Navigator.pop(context, kv.key);
|
|
},
|
|
child: Text(
|
|
kv.value,
|
|
style: const TextStyle(fontSize: 14, color: Colors.black),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
/// 日期选择器
|
|
Future<DateTime?> showMyDatePicker(
|
|
BuildContext context, CupertinoDatePickerMode mode,
|
|
{DateTime? defaultTime, DateTime? minTime, DateTime? maxTime}) async {
|
|
final DateTime? date = await showModalBottomSheet<DateTime>(
|
|
context: context,
|
|
builder: (context) {
|
|
DateTime pickedDate = DateTime.now();
|
|
return SizedBox(
|
|
height: 250,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
CupertinoButton(
|
|
child: const Text('取消'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
CupertinoButton(
|
|
child: const Text('确认'),
|
|
onPressed: () {
|
|
Navigator.pop(context, pickedDate);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const Divider(
|
|
height: 0,
|
|
thickness: 1,
|
|
),
|
|
Expanded(
|
|
child: CupertinoApp(
|
|
debugShowCheckedModeBanner: false,
|
|
locale: const Locale('zh', 'CN'),
|
|
localizationsDelegates: const [
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
supportedLocales: const [
|
|
Locale('zh', 'CN'),
|
|
Locale('en', 'US'),
|
|
],
|
|
home: CupertinoDatePicker(
|
|
mode: mode,
|
|
initialDateTime: defaultTime ?? DateTime.now(),
|
|
minimumDate: minTime,
|
|
maximumDate: maxTime,
|
|
onDateTimeChanged: (DateTime dateTime) {
|
|
pickedDate = dateTime;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
return date;
|
|
}
|
|
|
|
/// appBar Search Widget
|
|
PreferredSizeWidget makeAppBarSearchWidget(
|
|
BuildContext context,
|
|
String tipText,
|
|
ValueChanged<String> onSubmitted, {
|
|
double height = 56,
|
|
double? width,
|
|
TextInputType? textInputType,
|
|
List<TextInputFormatter>? inputFormatters,
|
|
ValueChanged<String>? onChanged,
|
|
TextEditingController? controller,
|
|
}) {
|
|
return PreferredSize(
|
|
child: SizedBox(
|
|
height: height,
|
|
child: Padding(
|
|
padding:
|
|
const EdgeInsets.only(top: 5, left: 10, right: 10, bottom: 10),
|
|
child: Container(
|
|
width: width ?? MediaQuery.of(context).size.width,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withAlpha(100),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 15),
|
|
child: const Icon(
|
|
Icons.search,
|
|
color: Colors.black45,
|
|
size: 20,
|
|
),
|
|
),
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 5),
|
|
width: MediaQuery.of(context).size.width -
|
|
15 * 2 -
|
|
15 -
|
|
20 -
|
|
5 -
|
|
(width ?? 0),
|
|
child: TextField(
|
|
controller: controller,
|
|
textInputAction: TextInputAction.search,
|
|
onSubmitted: onSubmitted,
|
|
keyboardType: textInputType,
|
|
inputFormatters: inputFormatters,
|
|
onChanged: onChanged,
|
|
decoration: InputDecoration(
|
|
hintText: tipText,
|
|
hintStyle: const TextStyle(
|
|
fontSize: 15, color: Colors.black45),
|
|
border: InputBorder.none,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
),
|
|
),
|
|
preferredSize: const Size(0, 42));
|
|
}
|
|
|
|
Widget makeSearchWidget(
|
|
BuildContext context, String hintText, ValueChanged<String> onSubmitted) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 5, left: 0, right: 0, bottom: 14),
|
|
child: Container(
|
|
height: 42,
|
|
width: MediaQuery.of(context).size.width,
|
|
decoration: BoxDecoration(
|
|
color: const Color.fromARGB(255, 247, 250, 255),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 15),
|
|
child: const Icon(
|
|
Icons.search,
|
|
color: Colors.black45,
|
|
size: 20,
|
|
),
|
|
),
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 5),
|
|
width: MediaQuery.of(context).size.width - 15 * 2 - 82,
|
|
child: TextField(
|
|
textInputAction: TextInputAction.search,
|
|
onSubmitted: onSubmitted,
|
|
onChanged: (str) {
|
|
if (str == "") {
|
|
onSubmitted("");
|
|
}
|
|
},
|
|
decoration: InputDecoration(
|
|
hintText: hintText,
|
|
hintStyle:
|
|
const TextStyle(fontSize: 15, color: Colors.black45),
|
|
border: InputBorder.none,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
);
|
|
}
|
|
|
|
class AlwaysDisabledFocusNode extends FocusNode {
|
|
@override
|
|
bool get hasFocus => false;
|
|
}
|