From 09385a4d770633be57301ab3e031bbb641dc2a9a Mon Sep 17 00:00:00 2001 From: louis <869322496@qq.com> Date: Wed, 20 Mar 2024 15:26:49 +0800 Subject: [PATCH] feat: develop core form field component --- lib/apis/api.dart | 7 + lib/constants/global_url.dart | 1 + lib/models/base_response.dart | 100 +++++++++-- lib/models/project.model.dart | 33 ++++ .../inventory_inout_controller.dart | 31 ++-- .../new_inventory_inout.dart | 157 +++++++++++++----- .../new_inventory_inout_controller.dart | 24 ++- lib/services/dio.service.dart | 12 +- lib/widgets/core/search_select.dart | 81 +++++++++ 9 files changed, 361 insertions(+), 85 deletions(-) create mode 100644 lib/models/project.model.dart create mode 100644 lib/widgets/core/search_select.dart diff --git a/lib/apis/api.dart b/lib/apis/api.dart index 7ed29ed..7186e99 100644 --- a/lib/apis/api.dart +++ b/lib/apis/api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:sk_base_mobile/constants/global_url.dart'; +import 'package:sk_base_mobile/models/base_response.dart'; import 'package:sk_base_mobile/services/dio.service.dart'; import '../constants/constants.dart'; @@ -20,6 +21,12 @@ Future getUserInfo() { ); } +// 获取项目信息 +Future> getProjects(Map params) { + return DioService.dio.get(Urls.getProjects, + queryParameters: {'page': 1, 'pageSize': 10, ...params}); +} + Future logout() { return DioService.dio.post( Urls.logout, diff --git a/lib/constants/global_url.dart b/lib/constants/global_url.dart index 46ded4a..741ebfb 100644 --- a/lib/constants/global_url.dart +++ b/lib/constants/global_url.dart @@ -6,5 +6,6 @@ class Urls { static String deleteAccount = 'user/deleteAccount'; static String saveUserInfo = 'user/saveUserInfo'; static String getUserInfo = 'account/profile'; + static String getProjects = 'project'; static String updateAvatar = 'user/updateAvatar'; } diff --git a/lib/models/base_response.dart b/lib/models/base_response.dart index 3bf97d5..7599119 100644 --- a/lib/models/base_response.dart +++ b/lib/models/base_response.dart @@ -1,31 +1,101 @@ +/// 封装http请求基础返回体 class BaseResponse { int? code; dynamic data; - bool? fail; - String? key; - String? msg; - bool? success; + String? message; - BaseResponse( - {this.code, this.data, this.fail, this.key, this.msg, this.success}); + BaseResponse({this.code, this.data, this.message}); BaseResponse.fromJson(Map json) { code = json['code']; - fail = json['fail']; - key = json['key']; data = json['data']; - msg = json['msg']; - success = json['success']; + message = json['message']; } Map toJson() { - final Map data = new Map(); + final Map data = {}; data['code'] = code; - data['fail'] = fail; data['data'] = data; - data['key'] = key; - data['msg'] = msg; - data['success'] = success; + data['message'] = message; return data; } } + +/// 封装http请求基础返回体 +class PaginationResponse { + int? code; + PaginationData? data; + String? message; + + PaginationResponse({this.code, this.data, this.message}); + + PaginationResponse.fromJson(Map json) { + code = json['code']; + data = json['data']; + message = json['message']; + } + + Map toJson() { + final Map data = {}; + data['code'] = code; + data['data'] = data; + data['message'] = message; + return data; + } +} + +class PaginationData { + PaginationData({ + required this.items, + required this.meta, + }); + + final List items; + final Meta? meta; + + factory PaginationData.fromJson(Map json) { + return PaginationData( + items: json["items"], + meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]), + ); + } + + Map toJson() => { + "items": items, + "meta": meta?.toJson(), + }; +} + +class Meta { + Meta({ + required this.totalItems, + required this.itemCount, + required this.itemsPerPage, + required this.totalPages, + required this.currentPage, + }); + + final int? totalItems; + final int? itemCount; + final int? itemsPerPage; + final int? totalPages; + final int? currentPage; + + factory Meta.fromJson(Map json) { + return Meta( + totalItems: json["totalItems"], + itemCount: json["itemCount"], + itemsPerPage: json["itemsPerPage"], + totalPages: json["totalPages"], + currentPage: json["currentPage"], + ); + } + + Map toJson() => { + "totalItems": totalItems, + "itemCount": itemCount, + "itemsPerPage": itemsPerPage, + "totalPages": totalPages, + "currentPage": currentPage, + }; +} diff --git a/lib/models/project.model.dart b/lib/models/project.model.dart new file mode 100644 index 0000000..9768bb6 --- /dev/null +++ b/lib/models/project.model.dart @@ -0,0 +1,33 @@ +class ProjectModel { + ProjectModel({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.name, + required this.isDelete, + }); + + final int? id; + final DateTime? createdAt; + final DateTime? updatedAt; + final String? name; + final int? isDelete; + + factory ProjectModel.fromJson(Map json) { + return ProjectModel( + id: json["id"], + createdAt: DateTime.tryParse(json["createdAt"] ?? ""), + updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""), + name: json["name"], + isDelete: json["isDelete"], + ); + } + + Map toJson() => { + "id": id, + "createdAt": createdAt?.toIso8601String(), + "updatedAt": updatedAt?.toIso8601String(), + "name": name, + "isDelete": isDelete, + }; +} diff --git a/lib/screens/inventory_inout/inventory_inout_controller.dart b/lib/screens/inventory_inout/inventory_inout_controller.dart index 7d7aaf9..1c009c1 100644 --- a/lib/screens/inventory_inout/inventory_inout_controller.dart +++ b/lib/screens/inventory_inout/inventory_inout_controller.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/db_helper/dbHelper.dart'; import 'package:sk_base_mobile/screens/new_inventory_inout/new_inventory_inout.dart'; +import 'package:sk_base_mobile/screens/new_inventory_inout/new_inventory_inout_controller.dart'; import 'package:sk_base_mobile/util/date.util.dart'; import 'package:sk_base_mobile/util/modal.util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; @@ -27,14 +28,8 @@ class InventoryInoutController extends GetxController { RxInt barIndex = 0.obs; RxList model = [].obs; final ScrollController scrollController = ScrollController(); - // decoration: const BoxDecoration( - // borderRadius: - // BorderRadius.all(Radius.circular(30)), - // gradient: LinearGradient(colors: [ - // AppTheme.primaryColorLight, - // AppTheme.primaryColor - // ]), - // ), + + /// 打开出库还是入库选择框 Future showInOrOutPickerDialog() async { showDialog( context: Get.overlayContext!, @@ -62,10 +57,7 @@ class InventoryInoutController extends GetxController { child: Ink( decoration: BoxDecoration( gradient: LinearGradient( - colors: [ - AppTheme.primaryColorLight, - AppTheme.primaryColor - ], + colors: [Colors.green[300]!, Colors.green], ), borderRadius: BorderRadius.all(Radius.circular(30)), @@ -130,15 +122,12 @@ class InventoryInoutController extends GetxController { const BorderRadius.all(Radius.circular(30)), child: Material( child: Ink( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( - colors: [ - AppTheme.primaryColorLight, - AppTheme.primaryColor - ], + colors: [Colors.red[300]!, Colors.red], ), borderRadius: - BorderRadius.all(Radius.circular(30)), + const BorderRadius.all(Radius.circular(30)), ), child: ElevatedButton( onPressed: () { @@ -167,7 +156,7 @@ class InventoryInoutController extends GetxController { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.add_circle_outline, + Icons.remove_circle_outline, color: AppTheme.nearlyWhite, size: ScreenAdaper.sp(80), ), @@ -212,7 +201,7 @@ class InventoryInoutController extends GetxController { return Container( margin: EdgeInsets.symmetric( horizontal: ScreenAdaper.width(100), - vertical: ScreenAdaper.height(100)), + vertical: ScreenAdaper.height(60)), child: ClipRRect( borderRadius: BorderRadius.circular(30), child: Material(child: NewInventoryInout())), @@ -228,7 +217,7 @@ class InventoryInoutController extends GetxController { child: child, ); }, - ) + ).then((value) => Get.delete()) : showModalBottomSheet( elevation: 0, isScrollControlled: true, diff --git a/lib/screens/new_inventory_inout/new_inventory_inout.dart b/lib/screens/new_inventory_inout/new_inventory_inout.dart index affcc4f..35515d0 100644 --- a/lib/screens/new_inventory_inout/new_inventory_inout.dart +++ b/lib/screens/new_inventory_inout/new_inventory_inout.dart @@ -4,12 +4,15 @@ import 'package:get/get.dart'; import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/constants/constants.dart'; import 'package:sk_base_mobile/models/product.model.dart'; +import 'package:sk_base_mobile/models/project.model.dart'; +import 'package:sk_base_mobile/widgets/core/search_select.dart'; +import 'package:sk_base_mobile/widgets/empty.dart'; import 'package:sk_base_mobile/widgets/gradient_button.dart'; import 'package:sk_base_mobile/screens/new_inventory_inout/new_inventory_inout_controller.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart'; class NewInventoryInout extends StatelessWidget { - String inOrOut; + final String inOrOut; NewInventoryInout({super.key, this.inOrOut = 'in'}); final controller = Get.put(NewInventoryInoutController()); @@ -89,8 +92,8 @@ class NewInventoryInout extends StatelessWidget { final children = [ Row(children: [ Expanded( - flex: 1, - child: buildInoutPicker(), + flex: 2, + child: buildProjectPicker(), ), SizedBox( width: ScreenAdaper.width(10), @@ -119,49 +122,113 @@ class NewInventoryInout extends StatelessWidget { )); } - Widget buildInoutPicker() { - // 选择下拉框表单项 两个选项 picker - return DropdownButtonFormField( - style: - TextStyle(fontSize: ScreenAdaper.sp(22), color: AppTheme.nearlyBlack), - // value: _dropdownValue, - decoration: InputDecoration( - floatingLabelBehavior: FloatingLabelBehavior.always, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))), - label: const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "*", - style: TextStyle(color: Colors.red), - ), - Text("出/入库"), - ]), - ), - onChanged: (String? newValue) { - // setState(() { - // _dropdownValue = newValue; - // }); - }, - icon: Icon( - Icons.arrow_drop_down_outlined, - size: ScreenAdaper.sp(40), - ), - items: [ - '入库', - '出库', - ].map>((String value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: ScreenAdaper.sp(30)), - ), - ); - }).toList(), + Widget buildProjectPicker() { + return SearchSelectComponent( + textController: controller.projectTextController, + labelText: '项目', + itemTextBuilder: (ProjectModel itemData) => + Text('${itemData?.name}'), ); + // return TypeAheadField( + // hideOnUnfocus: false, + // controller: controller.projectTextController, + // suggestionsCallback: (String keyword) { + // return controller.getProjects(keyword: keyword); + // }, + // builder: (context, _, focusNode) { + // return TextFormField( + // focusNode: focusNode, + // controller: _, + // decoration: InputDecoration( + // focusedBorder: OutlineInputBorder( + // borderSide: const BorderSide( + // color: AppTheme.primaryColorLight, width: 2), + // borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))), + // suffixIcon: _.text.isNotEmpty && focusNode.hasFocus + // ? IconButton( + // icon: const Icon(Icons.clear), + // // 当点击这个按钮时,清除TextFormField的值 + // onPressed: () => _.clear(), + // ) + // : const SizedBox(), + // floatingLabelBehavior: FloatingLabelBehavior.always, + // label: const Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // "*", + // style: TextStyle(color: Colors.red), + // ), + // Text( + // "项目", + // ), + // ]))); + // }, + // emptyBuilder: (_) => Container( + // alignment: Alignment.center, + // width: ScreenAdaper.width(200), + // height: ScreenAdaper.height(50), + // child: Text( + // '未找到项目', + // style: TextStyle(fontSize: ScreenAdaper.sp(20)), + // ), + // ), + // itemBuilder: (context, project) { + // return ListTile( + // title: Text(project.name ?? ''), + // ); + // }, + // onSelected: (ProjectModel project) { + // controller.projectTextController.text = project.name ?? ''; + // controller.payload['project'] = project.id; + // }); + // return Obx(() => DropdownButtonFormField( + // isExpanded: true, + // isDense: false, + // style: TextStyle( + // fontSize: ScreenAdaper.sp(22), color: AppTheme.nearlyBlack), + // // value: _dropdownValue, + // decoration: InputDecoration( + // contentPadding: EdgeInsets.symmetric( + // vertical: ScreenAdaper.height(10), + // horizontal: ScreenAdaper.width(10)), + // floatingLabelBehavior: FloatingLabelBehavior.always, + // border: OutlineInputBorder( + // borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))), + // label: const Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // "*", + // style: TextStyle(color: Colors.red), + // ), + // Text("项目"), + // ]), + // ), + // onChanged: (int? newValue) { + // // setState(() { + // // _dropdownValue = newValue; + // // }); + // }, + // // icon: Icon( + // // Icons.arrow_drop_down_outlined, + // // size: ScreenAdaper.sp(40), + // // ), + // items: controller.projects + // .map>((ProjectModel model) { + // return DropdownMenuItem( + // value: model.id, + // child: Text( + // model.name ?? '', + // style: TextStyle( + // fontSize: ScreenAdaper.sp(30), + // overflow: TextOverflow.ellipsis), + // ), + // ); + // }).toList(), + // )); } Widget buildProductPicker() { diff --git a/lib/screens/new_inventory_inout/new_inventory_inout_controller.dart b/lib/screens/new_inventory_inout/new_inventory_inout_controller.dart index 1648712..6e077ee 100644 --- a/lib/screens/new_inventory_inout/new_inventory_inout_controller.dart +++ b/lib/screens/new_inventory_inout/new_inventory_inout_controller.dart @@ -3,9 +3,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:sk_base_mobile/db_helper/dbHelper.dart'; +import 'package:sk_base_mobile/models/base_response.dart'; +import 'package:sk_base_mobile/models/project.model.dart'; import 'package:sk_base_mobile/models/task_model.dart'; import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout_controller.dart'; -import 'package:sk_base_mobile/util/util.dart'; +import 'package:sk_base_mobile/apis/api.dart' as Api; +import 'package:sk_base_mobile/util/date.util.dart'; +import 'package:sk_base_mobile/util/snack_bar.util.dart'; class NewInventoryInoutController extends GetxController { final formKey = GlobalKey(); @@ -25,6 +29,24 @@ class NewInventoryInoutController extends GetxController { final label = TextEditingController().obs; final description = TextEditingController().obs; final category = TextEditingController().obs; + RxList projects = [].obs; + final projectTextController = TextEditingController(); + Map payload = {}; + @override + void onReady() { + super.onReady(); + getProjects(); + } + + Future> getProjects({String? keyword}) async { + final res = + await Api.getProjects({'page': 1, 'pageSize': 10, 'name': keyword}); + if (res.data != null) { + projects.assignAll( + res.data!.items.map((e) => ProjectModel.fromJson(e)).toList()); + } + return projects; + } picStartTime(BuildContext context) async { var picker = diff --git a/lib/services/dio.service.dart b/lib/services/dio.service.dart index 7ffb7ac..409f049 100644 --- a/lib/services/dio.service.dart +++ b/lib/services/dio.service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:get/get.dart' as Get; import 'package:dio/dio.dart'; +import 'package:sk_base_mobile/models/base_response.dart'; import 'package:sk_base_mobile/util/logger_util.dart'; import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/services/storage.service.dart'; @@ -134,9 +135,14 @@ class DioService extends Get.GetxService { } if (response.data != null) { try { - dynamic responseData = response.data; - if (response.data is String) { - responseData = jsonDecode(response.data); + if (response.data['code'] == 200) { + if (GloablConfig.DEBUG) print(response.data['data']); + response.data = response.data['data']; + + // 分页数据处理 + if (response.data['meta'] != null && response.data['items'] != null) { + response.data = PaginationData.fromJson(response.data); + } } } catch (e) { printError(info: e.toString()); diff --git a/lib/widgets/core/search_select.dart b/lib/widgets/core/search_select.dart new file mode 100644 index 0000000..d551af7 --- /dev/null +++ b/lib/widgets/core/search_select.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:sk_base_mobile/app_theme.dart'; +import 'package:sk_base_mobile/util/screen_adaper_util.dart'; + +class SearchSelectComponent extends StatelessWidget { + final TextEditingController textController; + final Function? suggestionsCallback; + final Function? onSelected; + final String labelText; + final bool isRequired; + final Widget Function(T itemData)? itemTextBuilder; + const SearchSelectComponent( + {super.key, + required this.textController, + required this.labelText, + required this.itemTextBuilder, + this.suggestionsCallback, + this.onSelected, + this.isRequired = false}); + + @override + Widget build(BuildContext context) { + return TypeAheadField( + hideOnUnfocus: false, + controller: textController, + suggestionsCallback: (String keyword) { + return suggestionsCallback != null + ? suggestionsCallback!(keyword) + : []; + }, + builder: (context, _, focusNode) { + return TextFormField( + focusNode: focusNode, + controller: _, + decoration: InputDecoration( + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide( + color: AppTheme.primaryColorLight, width: 2), + borderRadius: BorderRadius.circular(ScreenAdaper.sp(15))), + suffixIcon: _.text.isNotEmpty && focusNode.hasFocus + ? IconButton( + icon: const Icon(Icons.clear), + // 当点击这个按钮时,清除TextFormField的值 + onPressed: () => _.clear(), + ) + : const SizedBox(), + floatingLabelBehavior: FloatingLabelBehavior.always, + label: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (isRequired) + const Text( + "*", + style: TextStyle(color: Colors.red), + ), + Text( + labelText, + ), + ]))); + }, + emptyBuilder: (_) => Container( + alignment: Alignment.center, + width: ScreenAdaper.width(200), + height: ScreenAdaper.height(50), + child: Text( + '未找到记录', + style: TextStyle(fontSize: ScreenAdaper.sp(20)), + ), + ), + itemBuilder: (context, itemData) { + return ListTile( + title: itemTextBuilder!(itemData), + ); + }, + onSelected: (selectedItemData) { + onSelected ?? onSelected!(selectedItemData); + }); + } +}