feat: develop base field form item widget

This commit is contained in:
louis 2024-04-09 16:32:18 +08:00
parent eabb746738
commit 865035e17f
13 changed files with 493 additions and 119 deletions

View File

@ -15,7 +15,13 @@ class Api {
);
}
///
///
static Future<Response<PaginationData>> getRoles(Map params) {
return DioService.dio.get<PaginationData>(Urls.roles,
queryParameters: {'page': 1, 'pageSize': 10, ...params});
}
///
static Future<Response> getDepts() {
return DioService.dio.get(Urls.depts);
}

View File

@ -117,6 +117,10 @@ final theme = ThemeData(
color: AppTheme.primaryColor,
fontSize: ScreenAdaper.height(30),
),
hintStyle: TextStyle(
color: AppTheme.grey,
fontSize: ScreenAdaper.height(25),
),
fillColor: AppTheme.inputFillColor,
filled: true,
labelStyle: TextStyle(

View File

@ -17,4 +17,5 @@ class Urls {
static String accountMenus = 'account/menus';
static String userInfo = 'system/users';
static String depts = 'system/depts';
static String roles = 'system/roles';
}

View File

@ -13,7 +13,7 @@ class RoleModel {
final int? id;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? name;
final String name;
final String? value;
final String? remark;
final int? status;

View File

@ -42,7 +42,6 @@ class DeptPicker extends StatelessWidget {
}
Future<List<CascadeItem<int, DeptModel>>> getData() async {
await Future.delayed(const Duration(milliseconds: 500));
try {
final res = await Api.getDepts();
if (res.data != null) {

View File

@ -5,12 +5,16 @@ import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/role.model.dart';
import 'package:sk_base_mobile/models/user_info.model.dart';
import 'package:sk_base_mobile/screens/hr_manage/components/dept_picker.dart';
import 'package:sk_base_mobile/util/logger_util.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/util/snack_bar.util.dart';
import 'package:sk_base_mobile/widgets/core/sk_cascade_picker.dart';
import 'package:sk_base_mobile/widgets/core/sk_dialog_header.dart';
import 'package:sk_base_mobile/widgets/core/sk_multi_picker.dart';
import 'package:sk_base_mobile/widgets/core/sk_multi_picker_dialog.dart';
import 'package:sk_base_mobile/widgets/core/sk_text_input.dart';
import 'package:sk_base_mobile/widgets/gradient_button.dart';
@ -51,6 +55,7 @@ class EditUserInfo extends StatelessWidget {
SkTextInput(
isDense: true,
textController: controller.nameEditController,
customLabel: true,
labelText: '姓名',
),
SizedBox(
@ -60,6 +65,7 @@ class EditUserInfo extends StatelessWidget {
///
SkTextInput(
isDense: true,
customLabel: true,
keyboardType: TextInputType.none,
textController: controller.deptEditController,
labelText: '所属部门',
@ -74,81 +80,48 @@ class EditUserInfo extends StatelessWidget {
),
///
SkTextInput(
SkMultiPickerDropdown(
isDense: true,
customLabel: true,
keyboardType: TextInputType.none,
textController: TextEditingController(),
textController: controller.roleEditController,
labelText: '角色',
hint: '选择角色',
onTap: (_) async {
Get.bottomSheet(Container(
height: ScreenAdaper.height(400),
decoration:
const BoxDecoration(color: AppTheme.nearlyWhite),
child: Column(children: [
Container(
padding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(20),
horizontal: ScreenAdaper.width(20)),
child: Row(
children: [
Text(
'取消',
style: TextStyle(
fontSize: ScreenAdaper.height(30)),
),
const Spacer(),
Text(
'确定',
style: TextStyle(
fontSize: ScreenAdaper.height(30),
color: AppTheme.primaryColor),
)
],
),
),
Expanded(
child: SingleChildScrollView(
child: ListBody(
children: <String>[
'角色1',
'角色2',
'角色3',
'角色4',
'角色5',
'角色6',
'角色7',
'角色8',
'角色9'
].map((String text) {
return Obx(
() => CheckboxListTile(
contentPadding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(0),
horizontal: ScreenAdaper.width(20)),
title: Text(text),
value:
controller.selectedDepts.contains(text),
onChanged: (bool? value) {
if (value == true) {
controller.selectedDepts.add(text);
} else {
controller.selectedDepts.remove(text);
}
},
),
);
}).toList(),
),
))
]),
));
Get.bottomSheet(SkMutiPickerDialog(
onSelected: (List<PickerItem> selectedData) {
controller.roleEditController.text;
}, getData: () async {
try {
final res =
await Api.getRoles({'page': 1, 'pageSize': 30});
if (res.data != null) {
List<PickerItem<int>> result =
res.data!.items.map<PickerItem<int>>((e) {
RoleModel data = RoleModel.fromJson(e);
return PickerItem(
label: data.name,
value: data.id!,
subLabel: data.remark,
checked: false);
}).toList();
return result;
}
return [];
} catch (e) {
LoggerUtil().error(e);
return [];
}
})).then(
(value) => Get.delete<SkMutiPickerDialogController>());
}),
SizedBox(
height: ScreenAdaper.height(defaultPadding),
),
SkTextInput(
isDense: true,
textController: TextEditingController(),
textController: controller.nickNameEditController,
customLabel: true,
labelText: '登录用户名',
),
SizedBox(
@ -156,9 +129,10 @@ class EditUserInfo extends StatelessWidget {
),
SkTextInput(
isDense: true,
customLabel: true,
textController: TextEditingController(),
labelText: '手机号',
)
),
]),
),
)),
@ -271,8 +245,9 @@ class EditUserInfoController extends GetxController {
EditUserInfoController(this.userId);
final nameEditController = TextEditingController();
final deptEditController = TextEditingController();
final roleEditController = TextEditingController();
final nickNameEditController = TextEditingController();
final userInfo = Rxn<UserInfoModel>();
RxList selectedDepts = RxList([]);
@override
onReady() {

View File

@ -0,0 +1,43 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
abstract class SkBaseFieldWidget extends StatelessWidget {
FocusNode focusNode = FocusNode();
late SkBaseFieldController baseFieldController;
final bool customLabel;
final String? labelText;
SkBaseFieldWidget({
super.key,
this.customLabel = false,
this.labelText,
autoFocus = false,
}) {
baseFieldController = Get.put(
SkBaseFieldController(autoFocus: autoFocus, focusNode: focusNode),
tag: '${labelText ?? Random().nextInt(1000)}',
);
}
}
class SkBaseFieldController extends GetxController {
RxBool isFocus = false.obs;
final bool autoFocus;
FocusNode? focusNode;
SkBaseFieldController({required this.autoFocus, this.focusNode});
@override
void onReady() {
if (autoFocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode?.requestFocus();
});
}
focusNode?.addListener(() {
isFocus.value = focusNode?.hasFocus ?? false;
});
super.onReady();
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_base_field.dart';
class SkFormItem extends StatelessWidget {
final Widget child;
final bool customLabel;
final bool isRequired;
final String? labelText;
final SkBaseFieldController controller;
const SkFormItem(
{super.key,
required this.child,
required this.customLabel,
required this.controller,
this.labelText,
this.isRequired = false});
@override
Widget build(BuildContext context) {
if (customLabel) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (isRequired)
Text(
"*",
style: TextStyle(
color: Colors.red, fontSize: ScreenAdaper.height(25)),
),
Obx(() => Text(
labelText ?? '',
style: TextStyle(
fontSize: ScreenAdaper.height(25),
color: controller.isFocus.value
? AppTheme.primaryColor
: AppTheme.nearlyBlack),
)),
]),
SizedBox(
height: ScreenAdaper.height(5),
),
child
],
);
} else {
return child;
}
}
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_base_field.dart';
import 'package:sk_base_mobile/widgets/core/sk_form_item.dart';
import 'package:sk_base_mobile/widgets/core/sk_tag.dart';
import 'package:multi_dropdown/multiselect_dropdown.dart';
class SkMultiPickerDropdown<T> extends SkBaseFieldWidget {
final TextEditingController textController;
final Function(FocusNode)? onTap;
final bool isRequired;
final String? labelText;
final String? hint;
final bool isTextArea;
final bool isDense;
final Function(String)? onTapOutside;
final Function(String)? onChanged;
final String? Function(String?)? validator;
final EdgeInsetsGeometry? contentPadding;
final ValueChanged<String>? onFieldSubmitted;
final Icon? prefix;
final Widget? suffixIcon;
final InputBorder? border;
final FloatingLabelBehavior? floatingLabelBehavior;
final TextInputType? keyboardType;
SkMultiPickerDropdown(
{super.key,
required this.textController,
super.customLabel = false,
super.autoFocus = false,
this.onTap,
this.hint,
this.onFieldSubmitted,
this.isRequired = false,
this.onTapOutside,
this.keyboardType,
this.labelText,
this.prefix,
this.suffixIcon,
this.onChanged,
this.border,
this.floatingLabelBehavior = FloatingLabelBehavior.always,
this.isTextArea = false,
this.contentPadding,
this.isDense = false,
this.validator});
@override
Widget build(BuildContext context) {
return SkFormItem(
child: MultiSelectDropDown<int>(
focusNode: focusNode,
onOptionSelected: (List<ValueItem> selectedOptions) {},
options: <ValueItem<int>>[
ValueItem(label: 'Option 1', value: 1),
ValueItem(label: 'Option 2', value: 2),
ValueItem(label: 'Option 3', value: 3),
ValueItem(label: 'Option 4', value: 4),
ValueItem(label: 'Option 5', value: 5),
ValueItem(label: 'Option 6', value: 6),
],
borderColor: AppTheme.nearlyBlack,
fieldBackgroundColor: AppTheme.inputFillColor,
borderWidth: 1,
borderRadius: ScreenAdaper.sp(15),
focusedBorderWidth: 2,
focusedBorderColor: AppTheme.primaryColorLight,
hint: '请选择',
hintStyle: Theme.of(Get.context!).inputDecorationTheme.hintStyle,
hintPadding: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)),
selectionType: SelectionType.multi,
chipConfig: const ChipConfig(wrapType: WrapType.scroll),
optionTextStyle: const TextStyle(fontSize: 16),
selectedOptionIcon: const Icon(Icons.check_circle),
),
customLabel: customLabel,
labelText: labelText,
isRequired: isRequired,
controller: baseFieldController);
// return DropdownButtonFormField<String>(
// focusNode: focusNode,
// onTap: () {
// if (widget.onTap != null) {
// widget.onTap!(focusNode);
// }
// },
// icon: Icon(Icons.arrow_drop_down),
// selectedItemBuilder: (context) => [
// Container(
// width: ScreenAdaper.width(200),
// child: SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(mainAxisSize: MainAxisSize.min, children: [
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// SkTag(text: '角色', color: AppTheme.primaryColor),
// ]),
// ),
// )
// ],
// autovalidateMode: AutovalidateMode.onUserInteraction,
// decoration: InputDecoration(
// prefixIcon: widget.prefix,
// errorStyle: const TextStyle(fontSize: 0, height: 0.01),
// contentPadding: widget.contentPadding,
// isDense: widget.isDense,
// border: widget.border,
// floatingLabelBehavior: widget.floatingLabelBehavior,
// label: widget.labelText != null
// ? Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
// children: [
// if (widget.isRequired)
// Text(
// "*",
// style: TextStyle(
// color: Colors.red,
// fontSize: ScreenAdaper.height(30)),
// ),
// Text(
// widget.labelText!,
// style: TextStyle(fontSize: ScreenAdaper.height(30)),
// ),
// ])
// : null,
// focusedBorder: OutlineInputBorder(
// borderSide:
// const BorderSide(color: AppTheme.primaryColorLight, width: 2),
// borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))),
// hintText: widget.hint ?? '请输入',
// ),
// items: [],
// onChanged: (Object? value) {},
// );
}
}

View File

@ -0,0 +1,136 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/util/snack_bar.util.dart';
import 'package:sk_base_mobile/widgets/loading_indicator.dart';
class SkMutiPickerDialog<T> extends StatelessWidget {
final SkMutiPickerDialogController controller;
Function(List<PickerItem> data)? onSelected;
SkMutiPickerDialog(
{super.key,
required Future<List<PickerItem<T>>> Function() getData,
this.onSelected})
: controller = Get.put(SkMutiPickerDialogController<T>(getData: getData));
@override
Widget build(BuildContext context) {
return Container(
height: ScreenAdaper.height(400),
decoration: const BoxDecoration(color: AppTheme.nearlyWhite),
child: Column(children: [
Container(
padding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(20),
horizontal: ScreenAdaper.width(20)),
child: Row(
children: [
Text(
'取消',
style: TextStyle(fontSize: ScreenAdaper.height(30)),
),
const Spacer(),
GestureDetector(
onTap: () {
if (onSelected != null) {
final selectedData = controller.pickData
.where((element) => element.checked)
.toList();
if (selectedData.isEmpty) {
SnackBarUtil().warning('请最少选择一个“员工”角色作为基础角色');
return;
}
onSelected!(selectedData);
}
},
child: Text(
'确定',
style: TextStyle(
fontSize: ScreenAdaper.height(30),
color: AppTheme.primaryColor),
),
)
],
),
),
Expanded(
child: Obx(
() => controller.loading.value
? const LoadingIndicator(
common: true,
)
: SingleChildScrollView(
child: ListBody(
children: controller.pickData.mapIndexed((
int index,
PickerItem item,
) {
return CheckboxListTile(
fillColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return AppTheme.primaryColor;
}
return null;
}),
dense: true,
visualDensity: VisualDensity.compact,
contentPadding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(0),
horizontal: ScreenAdaper.width(20)),
title: Text(item.label,
style: TextStyle(fontSize: ScreenAdaper.height(25))),
subtitle: item.subLabel == null
? null
: Text(
item.subLabel!,
style: TextStyle(
fontSize: ScreenAdaper.height(20),
color: AppTheme.grey),
),
value: controller.pickData[index].checked,
onChanged: (bool? value) {
PickerItem item = controller.pickData[index];
item.checked = value ?? false;
controller.pickData[index] = item;
},
);
}).toList(),
)),
))
]),
);
}
}
class SkMutiPickerDialogController<T> extends GetxController {
RxList<PickerItem<T>> pickData = RxList([]);
Future<List<PickerItem<T>>> Function() getData;
SkMutiPickerDialogController({required this.getData});
RxBool loading = false.obs;
@override
void onReady() {
init();
super.onReady();
}
Future<void> init() async {
loading.value = true;
pickData.assignAll(await getData());
loading.value = false;
}
}
class PickerItem<T> {
final String label;
final String? subLabel;
final T value;
bool checked;
PickerItem(
{required this.label,
required this.value,
required this.checked,
this.subLabel});
}

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_base_field.dart';
import 'package:sk_base_mobile/widgets/core/sk_form_item.dart';
class SkTextInput extends StatefulWidget {
class SkTextInput extends SkBaseFieldWidget {
final TextEditingController textController;
final Function(FocusNode)? onTap;
final bool isRequired;
final String? labelText;
final String? hint;
final bool isTextArea;
final bool isDense;
@ -14,15 +15,19 @@ class SkTextInput extends StatefulWidget {
final Function(String)? onChanged;
final String? Function(String?)? validator;
final EdgeInsetsGeometry? contentPadding;
final bool autoFocus;
final ValueChanged<String>? onFieldSubmitted;
final Icon? prefix;
final Widget? suffixIcon;
final InputBorder? border;
final FloatingLabelBehavior? floatingLabelBehavior;
final TextInputType? keyboardType;
const SkTextInput(
SkTextInput(
{super.key,
super.customLabel = false,
super.autoFocus = false,
super.labelText,
required this.textController,
this.onTap,
this.hint,
@ -30,76 +35,57 @@ class SkTextInput extends StatefulWidget {
this.isRequired = false,
this.onTapOutside,
this.keyboardType,
this.labelText,
this.prefix,
this.suffixIcon,
this.onChanged,
this.border,
this.floatingLabelBehavior = FloatingLabelBehavior.always,
this.isTextArea = false,
this.autoFocus = false,
this.contentPadding,
this.isDense = false,
this.validator});
@override
State<SkTextInput> createState() => _SkTextInputState();
}
class _SkTextInputState extends State<SkTextInput> {
late FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
if (widget.autoFocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
}
@override
Widget build(BuildContext context) {
return TextFormField(
Widget field = TextFormField(
focusNode: focusNode,
controller: widget.textController,
controller: textController,
onChanged: (String value) {
if (widget.onChanged != null) {
widget.onChanged!(value);
if (onChanged != null) {
onChanged!(value);
}
},
onTapOutside: (event) {
if (widget.onTapOutside != null) {
widget.onTapOutside!(widget.textController.text);
if (onTapOutside != null) {
onTapOutside!(textController.text);
FocusScope.of(context).unfocus();
}
},
maxLines: widget.isTextArea ? 2 : 1, //
maxLines: isTextArea ? 2 : 1, //
onTap: () {
if (widget.onTap != null) {
widget.onTap!(focusNode);
if (onTap != null) {
onTap!(focusNode);
}
},
keyboardType: widget.keyboardType,
onFieldSubmitted: widget.onFieldSubmitted,
keyboardType: keyboardType,
onFieldSubmitted: onFieldSubmitted,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: widget.validator,
validator: validator,
decoration: InputDecoration(
prefixIcon: widget.prefix,
suffixIcon: widget.suffixIcon,
prefixIcon: prefix,
suffixIcon: suffixIcon,
errorStyle: const TextStyle(fontSize: 0, height: 0.01),
contentPadding: widget.contentPadding,
isDense: widget.isDense,
border: widget.border,
floatingLabelBehavior: widget.floatingLabelBehavior,
label: widget.labelText != null
contentPadding: contentPadding,
isDense: isDense,
border: border,
floatingLabelBehavior: floatingLabelBehavior,
label: labelText != null && !customLabel
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.isRequired)
if (isRequired)
Text(
"*",
style: TextStyle(
@ -107,16 +93,28 @@ class _SkTextInputState extends State<SkTextInput> {
fontSize: ScreenAdaper.height(30)),
),
Text(
widget.labelText!,
labelText!,
style: TextStyle(fontSize: ScreenAdaper.height(30)),
),
])
: null,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppTheme.primaryColorLight, width: 2),
borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))),
hintText: widget.hint ?? '请输入',
hintText: hint ?? '请输入',
),
);
return SkFormItem(
controller: baseFieldController,
customLabel: customLabel,
labelText: labelText,
isRequired: isRequired,
child: field,
);
}
}
// class SkTextInputController extends GetxController {
// RxBool isFocus = false.obs;
// @override
// void onReady() {
// super.onReady();
// }
// }

View File

@ -608,6 +608,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
multi_dropdown:
dependency: "direct main"
description:
name: multi_dropdown
sha256: b63ff339fcc875d667f8688c8ef62853545b580dd2b6fe78b73339783268afd8
url: "https://pub.dev"
source: hosted
version: "2.1.4"
octo_image:
dependency: transitive
description:

View File

@ -68,6 +68,7 @@ dependencies:
math_expressions: ^2.4.0
install_plugin: ^2.1.0
url_launcher: ^6.2.5
multi_dropdown: ^2.1.4
dev_dependencies:
flutter_test:
sdk: flutter