feat: develop core form field component

This commit is contained in:
louis 2024-03-20 15:26:49 +08:00
parent 7f309e999b
commit 09385a4d77
9 changed files with 361 additions and 85 deletions

View File

@ -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<Response> getUserInfo() {
);
}
//
Future<Response<PaginationData>> getProjects(Map params) {
return DioService.dio.get<PaginationData>(Urls.getProjects,
queryParameters: {'page': 1, 'pageSize': 10, ...params});
}
Future<Response> logout() {
return DioService.dio.post(
Urls.logout,

View File

@ -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';
}

View File

@ -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<String, dynamic> json) {
code = json['code'];
fail = json['fail'];
key = json['key'];
data = json['data'];
msg = json['msg'];
success = json['success'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
final Map<String, dynamic> data = <String, dynamic>{};
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<String, dynamic> json) {
code = json['code'];
data = json['data'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['code'] = code;
data['data'] = data;
data['message'] = message;
return data;
}
}
class PaginationData {
PaginationData({
required this.items,
required this.meta,
});
final List<dynamic> items;
final Meta? meta;
factory PaginationData.fromJson(Map<String, dynamic> json) {
return PaginationData(
items: json["items"],
meta: json["meta"] == null ? null : Meta.fromJson(json["meta"]),
);
}
Map<String, dynamic> 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<String, dynamic> json) {
return Meta(
totalItems: json["totalItems"],
itemCount: json["itemCount"],
itemsPerPage: json["itemsPerPage"],
totalPages: json["totalPages"],
currentPage: json["currentPage"],
);
}
Map<String, dynamic> toJson() => {
"totalItems": totalItems,
"itemCount": itemCount,
"itemsPerPage": itemsPerPage,
"totalPages": totalPages,
"currentPage": currentPage,
};
}

View File

@ -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<String, dynamic> json) {
return ProjectModel(
id: json["id"],
createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
name: json["name"],
isDelete: json["isDelete"],
);
}
Map<String, dynamic> toJson() => {
"id": id,
"createdAt": createdAt?.toIso8601String(),
"updatedAt": updatedAt?.toIso8601String(),
"name": name,
"isDelete": isDelete,
};
}

View File

@ -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<void> 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<NewInventoryInoutController>())
: showModalBottomSheet(
elevation: 0,
isScrollControlled: true,

View File

@ -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<String>(
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: <String>[
'入库',
'出库',
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: ScreenAdaper.sp(30)),
),
);
}).toList(),
Widget buildProjectPicker() {
return SearchSelectComponent(
textController: controller.projectTextController,
labelText: '项目',
itemTextBuilder: <ProjectModel>(ProjectModel itemData) =>
Text('${itemData?.name}'),
);
// return TypeAheadField<ProjectModel>(
// 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<int>(
// 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<DropdownMenuItem<int>>((ProjectModel model) {
// return DropdownMenuItem<int>(
// value: model.id,
// child: Text(
// model.name ?? '',
// style: TextStyle(
// fontSize: ScreenAdaper.sp(30),
// overflow: TextOverflow.ellipsis),
// ),
// );
// }).toList(),
// ));
}
Widget buildProductPicker() {

View File

@ -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<FormState>();
@ -25,6 +29,24 @@ class NewInventoryInoutController extends GetxController {
final label = TextEditingController().obs;
final description = TextEditingController().obs;
final category = TextEditingController().obs;
RxList<ProjectModel> projects = <ProjectModel>[].obs;
final projectTextController = TextEditingController();
Map<String, dynamic> payload = {};
@override
void onReady() {
super.onReady();
getProjects();
}
Future<List<ProjectModel>> 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 =

View File

@ -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());

View File

@ -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>(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);
});
}
}