feat: product searcher

This commit is contained in:
louis 2024-03-25 14:16:00 +08:00
parent 9e39aa8c41
commit 7986fde5d3
28 changed files with 858 additions and 86 deletions

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:sk_base_mobile/constants/global_url.dart'; import 'package:sk_base_mobile/constants/global_url.dart';
import 'package:sk_base_mobile/models/base_response.dart'; import 'package:sk_base_mobile/models/base_response.dart';
import 'package:sk_base_mobile/models/dict_type.model.dart';
import 'package:sk_base_mobile/services/dio.service.dart'; import 'package:sk_base_mobile/services/dio.service.dart';
import '../constants/constants.dart'; import '../constants/constants.dart';
@ -39,6 +40,11 @@ Future<Response<PaginationData>> getInventoryInout(Map params) {
queryParameters: {'page': 1, 'pageSize': 10, ...(params)}); queryParameters: {'page': 1, 'pageSize': 10, ...(params)});
} }
// ()
Future<Response> getDictTypeAll(Map params) {
return DioService.dio.post(Urls.getDictType, data: params);
}
Future<Response> logout() { Future<Response> logout() {
return DioService.dio.post( return DioService.dio.post(
Urls.logout, Urls.logout,

View File

@ -0,0 +1,5 @@
class DictTypeEnum {
static const String InventoryRoom = "inventory_room";
static const String InventoryLine = 'inventory_line';
static const String InventoryLineLevel = 'inventory_line_level';
}

View File

@ -10,4 +10,5 @@ class Urls {
static String getProducts = 'product'; static String getProducts = 'product';
static String getInventoryInout = 'materials-in-out'; static String getInventoryInout = 'materials-in-out';
static String updateAvatar = 'user/updateAvatar'; static String updateAvatar = 'user/updateAvatar';
static String getDictType = 'system/dict-type/all';
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/store/dict.store.dart';
import 'package:sk_base_mobile/util/loading_util.dart'; import 'package:sk_base_mobile/util/loading_util.dart';
import 'package:timeago/timeago.dart' as timeago; import 'package:timeago/timeago.dart' as timeago;
import 'package:sk_base_mobile/constants/cache_key.dart'; import 'package:sk_base_mobile/constants/cache_key.dart';
@ -42,6 +43,9 @@ class Global {
/// ///
await Get.putAsync<AuthStore>(() => AuthStore().init()); await Get.putAsync<AuthStore>(() => AuthStore().init());
///
await Get.putAsync<DictService>(() => DictService().init());
if (StorageService.to.getString(CacheKeys.token, isWithUser: false) != if (StorageService.to.getString(CacheKeys.token, isWithUser: false) !=
null) { null) {
/// ///

View File

@ -5,7 +5,7 @@ import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/services/storage.service.dart'; import 'package:sk_base_mobile/services/storage.service.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/widgets/refresh-footer.dart'; import 'package:sk_base_mobile/widgets/refresh_footer.dart';
import 'package:sk_base_mobile/widgets/refresh_header.dart'; import 'package:sk_base_mobile/widgets/refresh_header.dart';
import 'constants/constants.dart'; import 'constants/constants.dart';

View File

@ -1,15 +1,15 @@
class DictItemModel { class DictItemModel {
DictItemModel({ DictItemModel({
required this.id, this.id,
required this.createdAt, this.createdAt,
required this.updatedAt, this.updatedAt,
required this.creator, this.creator,
required this.updater, this.updater,
required this.label, required this.label,
required this.value, this.value,
required this.orderNo, this.orderNo,
required this.status, this.status,
required this.remark, this.remark,
}); });
final int? id; final int? id;
@ -17,7 +17,7 @@ class DictItemModel {
final DateTime? updatedAt; final DateTime? updatedAt;
final String? creator; final String? creator;
final String? updater; final String? updater;
final String? label; final String label;
final String? value; final String? value;
final int? orderNo; final int? orderNo;
final int? status; final int? status;

View File

@ -0,0 +1,58 @@
import 'dict_item.model.dart';
class DictTypeModel {
DictTypeModel({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.creator,
required this.updater,
required this.name,
required this.code,
required this.status,
required this.remark,
required this.dictItems,
});
final int? id;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? creator;
final String? updater;
final String? name;
final String? code;
final int? status;
final String? remark;
final List<DictItemModel> dictItems;
factory DictTypeModel.fromJson(Map<String, dynamic> json) {
return DictTypeModel(
id: json["id"],
createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
creator: json["creator"],
updater: json["updater"],
name: json["name"],
code: json["code"],
status: json["status"],
remark: json["remark"],
dictItems: json["dictItems"] == null
? []
: List<DictItemModel>.from(
json["dictItems"]!.map((x) => DictItemModel.fromJson(x))),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"createdAt": createdAt?.toIso8601String(),
"updatedAt": updatedAt?.toIso8601String(),
"creator": creator,
"updater": updater,
"name": name,
"code": code,
"status": status,
"remark": remark,
"dictItems": dictItems.map((x) => x.toJson()).toList(),
};
}

View File

@ -4,8 +4,8 @@ import 'package:sk_base_mobile/models/file.model.dart';
import 'company.model.dart'; import 'company.model.dart';
class ProductModel { class ProductModel {
ProductModel({ ProductModel(
required this.id, {required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.name, required this.name,
@ -17,9 +17,12 @@ class ProductModel {
required this.files, required this.files,
required this.company, required this.company,
required this.unit, required this.unit,
}); this.productNumber,
this.productSpecification});
final int? id; final int? id;
final String? productNumber;
final String? productSpecification;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final String? name; final String? name;
@ -41,6 +44,8 @@ class ProductModel {
remark: json["remark"], remark: json["remark"],
isDelete: json["isDelete"], isDelete: json["isDelete"],
companyId: json["companyId"], companyId: json["companyId"],
productNumber: json['productNumber'],
productSpecification: json['productSpecification'],
unitId: json["unitId"], unitId: json["unitId"],
namePinyin: json["namePinyin"], namePinyin: json["namePinyin"],
files: json["files"] == null files: json["files"] == null
@ -63,6 +68,8 @@ class ProductModel {
"isDelete": isDelete, "isDelete": isDelete,
"companyId": companyId, "companyId": companyId,
"unitId": unitId, "unitId": unitId,
'productNumber': productNumber,
'productSpecification': productSpecification,
"namePinyin": namePinyin, "namePinyin": namePinyin,
"files": files.map((x) => x?.toJson()).toList(), "files": files.map((x) => x?.toJson()).toList(),
"company": company?.toJson(), "company": company?.toJson(),

View File

@ -3,14 +3,17 @@ import 'package:get/get.dart';
import 'package:sk_base_mobile/screens/inventory_inout/components/task_list.dart'; import 'package:sk_base_mobile/screens/inventory_inout/components/task_list.dart';
import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout_controller.dart'; import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout_controller.dart';
class TaskPageView extends StatelessWidget { class InventoryInoutView extends StatelessWidget {
TaskPageView({super.key}); InventoryInoutView({super.key});
final controller = Get.put(InventoryInoutController()); final controller = Get.put(InventoryInoutController());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PageView( return PageView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
reverse: true, reverse: true,
onPageChanged: (index) {
controller.changePage(index);
},
controller: controller.pageController, controller: controller.pageController,
children: List.generate( children: List.generate(
controller.daysNum, controller.daysNum,

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sk_base_mobile/screens/inventory_inout/components/change_button_roe.dart'; import 'package:sk_base_mobile/screens/inventory_inout/components/change_button_roe.dart';
import 'package:sk_base_mobile/screens/inventory_inout/components/task_page_View.dart'; import 'package:sk_base_mobile/screens/inventory_inout/components/inventory_inout_page_View.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart';
class TaskPageBody extends StatelessWidget { class TaskPageBody extends StatelessWidget {
@ -38,7 +38,7 @@ class TaskPageBody extends StatelessWidget {
Colors.white.withOpacity(.0), Colors.white.withOpacity(.0),
Colors.white.withOpacity(.0), Colors.white.withOpacity(.0),
])), ])),
child: TaskPageView(), child: InventoryInoutView(),
)); ));
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/db_helper/dbHelper.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.dart';
import 'package:sk_base_mobile/util/date.util.dart'; import 'package:sk_base_mobile/util/date.util.dart';
import 'package:sk_base_mobile/util/logger_util.dart';
import 'package:sk_base_mobile/util/modal.util.dart'; import 'package:sk_base_mobile/util/modal.util.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/apis/api.dart' as Api; import 'package:sk_base_mobile/apis/api.dart' as Api;
@ -36,6 +37,12 @@ class InventoryInoutController extends GetxController {
// getInoutHistory(); // getInoutHistory();
} }
Future<void> changePage(int index) async {
LoggerUtil()
.info(DateUtil.format(DateTime.now().add(Duration(days: -index))));
}
/// ///
Future<void> showInOrOutPickerDialog() async { Future<void> showInOrOutPickerDialog() async {
showDialog( showDialog(

View File

@ -0,0 +1,149 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/models/index.dart';
import 'package:sk_base_mobile/util/debouncer.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/apis/api.dart' as Api;
class ProductSearch extends StatelessWidget {
ProductSearch({super.key});
final controller = Get.put(ProductSearchController());
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: ScreenAdaper.width(20),
vertical: ScreenAdaper.height(20)),
child: Column(
children: [buildSearchBar(), Expanded(child: buildProductList())],
),
);
}
Widget buildSearchBar() {
final doSearch = debouncer((_) {
controller.onRefresh();
}, delayTime: 500);
return Row(
children: [
Expanded(
child: TextField(
controller: controller.searchBarController,
onChanged: (value) => doSearch(value),
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(10),
horizontal: ScreenAdaper.width(10)),
hintText: '名称,编号',
floatingLabelBehavior: FloatingLabelBehavior.always,
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
))),
],
);
}
Widget buildProductList() {
return Obx(
() => SmartRefresher(
enablePullDown: true,
enablePullUp: true,
controller: controller.refreshController,
onLoading: controller.onLoading,
onRefresh: controller.onRefresh,
child: ListView.separated(
itemCount: controller.products.length,
separatorBuilder: (context, index) => const Divider(
height: 1,
color: AppTheme.dividerColor,
),
itemBuilder: (context, index) {
final itemData = controller.products[index];
return InkWell(
onTap: () {},
child: Container(
padding:
EdgeInsets.symmetric(vertical: ScreenAdaper.height(10)),
child: Row(
children: [
Text(
itemData.productNumber!,
style: TextStyle(fontSize: ScreenAdaper.sp(20)),
),
SizedBox(
width: ScreenAdaper.width(10),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${itemData.name}',
style: TextStyle(fontSize: ScreenAdaper.sp(20)),
),
Text(
'${itemData.company?.name}',
style: TextStyle(
fontSize: ScreenAdaper.sp(15),
color: AppTheme.grey),
)
],
)),
],
),
),
);
}),
),
);
}
}
class ProductSearchController extends GetxController {
RxList<ProductModel> products = RxList([]);
TextEditingController searchBarController = TextEditingController(text: '');
RefreshController refreshController = RefreshController(initialRefresh: true);
int page = 1;
int limit = 15;
int total = 0;
Future<List<ProductModel>> getData({bool isRefresh = false}) async {
if (isRefresh == true) {
page = 1;
} else {
page++;
}
final res = await Api.getProducts(
{'page': page, 'pageSize': 30, 'keyword': searchBarController.text});
List<ProductModel> newList =
res.data!.items.map((e) => ProductModel.fromJson(e)).toList();
isRefresh == true ? products.assignAll(newList) : products.addAll(newList);
return newList;
}
Future<void> onRefresh() async {
await getData(isRefresh: true).then((_) {
refreshController.refreshCompleted(resetFooterState: true);
}).catchError((_) {
refreshController.refreshFailed();
});
}
Future<void> onLoading() async {
await getData().then((_) {
if (_.isEmpty) {
refreshController.loadNoData();
} else {
refreshController.loadComplete();
}
}).catchError((_) {
refreshController.loadFailed();
});
}
}

View File

@ -1,17 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/constants.dart'; import 'package:sk_base_mobile/constants/constants.dart';
import 'package:sk_base_mobile/constants/dict_enum.dart';
import 'package:sk_base_mobile/models/index.dart'; import 'package:sk_base_mobile/models/index.dart';
import 'package:sk_base_mobile/screens/new_inventory_inout/components/date_time.dart'; import 'package:sk_base_mobile/screens/new_inventory_inout/components/date_time.dart';
import 'package:sk_base_mobile/screens/new_inventory_inout/components/product_search.dart';
import 'package:sk_base_mobile/store/dict.store.dart';
import 'package:sk_base_mobile/util/date.util.dart'; import 'package:sk_base_mobile/util/date.util.dart';
import 'package:sk_base_mobile/util/modal.util.dart';
import 'package:sk_base_mobile/widgets/core/zt_bottomsheet_picker.dart';
import 'package:sk_base_mobile/widgets/core/zt_number_input.dart'; import 'package:sk_base_mobile/widgets/core/zt_number_input.dart';
import 'package:sk_base_mobile/widgets/core/zt_search_select.dart'; import 'package:sk_base_mobile/widgets/core/zt_search_select.dart';
import 'package:sk_base_mobile/widgets/core/zk_date_picker.dart'; import 'package:sk_base_mobile/widgets/core/zk_date_picker.dart';
import 'package:sk_base_mobile/widgets/core/zt_text_input.dart'; import 'package:sk_base_mobile/widgets/core/zt_text_input.dart';
import 'package:sk_base_mobile/widgets/empty.dart';
import 'package:sk_base_mobile/widgets/gradient_button.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/screens/new_inventory_inout/new_inventory_inout_controller.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart';
@ -21,7 +28,7 @@ class NewInventoryInout extends StatelessWidget {
NewInventoryInout({super.key, this.inOrOut = 'in'}); NewInventoryInout({super.key, this.inOrOut = 'in'});
final controller = Get.put(NewInventoryInoutController()); final controller = Get.put(NewInventoryInoutController());
final dictService = Get.find<DictService>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
@ -88,12 +95,13 @@ class NewInventoryInout extends StatelessWidget {
SizedBox( SizedBox(
height: ScreenAdaper.height(formVerticalGap), height: ScreenAdaper.height(formVerticalGap),
), ),
Row( buildPositionBottomPicker(),
children: [Expanded(child: buildRemark())],
),
SizedBox( SizedBox(
height: ScreenAdaper.height(formVerticalGap), height: ScreenAdaper.height(formVerticalGap),
), ),
Row(
children: [Expanded(child: buildRemark())],
),
buildImageUploadQueue() buildImageUploadQueue()
]; ];
@ -116,16 +124,16 @@ class NewInventoryInout extends StatelessWidget {
/// ///
Widget buildProjectPicker() { Widget buildProjectPicker() {
return ZtSearchSelect( return ZtSearchSelect<ProjectModel>(
isRequired: true, isRequired: true,
textController: controller.projectTextController, textController: controller.projectTextController,
labelText: '项目', labelText: '项目',
itemBuilder: (itemData) => Container( itemBuilder: (_, itemData) => Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: ScreenAdaper.height(15), vertical: ScreenAdaper.height(15),
horizontal: ScreenAdaper.width(20)), horizontal: ScreenAdaper.width(20)),
child: Text( child: Text(
'${itemData?.name}', '${itemData.name}',
style: TextStyle(fontSize: ScreenAdaper.sp(20)), style: TextStyle(fontSize: ScreenAdaper.sp(20)),
)), )),
suggestionsCallback: (String keyword) { suggestionsCallback: (String keyword) {
@ -142,24 +150,168 @@ class NewInventoryInout extends StatelessWidget {
/// ///
Widget buildProductPicker() { Widget buildProductPicker() {
return ZtSearchSelect( return TextFormField(
isRequired: true, controller: controller.positionTextController,
textController: controller.productTextController, decoration: const InputDecoration(
labelText: '产品', labelText: '产品',
itemBuilder: (itemData) => ListTile( floatingLabelBehavior: FloatingLabelBehavior.always),
title: Text('${itemData?.name}'), readOnly: true,
subtitle: Text('${itemData?.company?.name ?? ''}'), onTap: () {
Get.generalDialog(
barrierLabel: "productPicker",
barrierDismissible: true,
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (_, __, ___) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: Material(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(
30,
), ),
suggestionsCallback: (String keyword) { topRight: Radius.circular(30)),
return controller.getProducts(keyword: keyword); child: Container(
height: Get.height - ScreenAdaper.height(200),
width: Get.width - ScreenAdaper.width(200),
child: ProductSearch(),
),
))));
}, },
onClear: () { transitionBuilder: (_, anim, __, child) {
controller.payload.remove('productId'); Tween<Offset> tween;
tween = Tween(begin: const Offset(0, 1), end: Offset.zero);
return SlideTransition(
position: tween.animate(
CurvedAnimation(parent: anim, curve: Curves.easeInOut),
),
child: child,
);
}, },
onSelected: (ProductModel product) { );
controller.productTextController.text = product.name ?? '';
controller.payload['productId'] = product.id;
}); });
// return DropdownSearch<ProductModel>(
// asyncItems: (String filter) async {
// return controller.getProducts(keyword: filter);
// },
// dropdownDecoratorProps: const DropDownDecoratorProps(
// dropdownSearchDecoration: InputDecoration(
// labelText: "产品",
// hintText: "请选择",
// floatingLabelBehavior: FloatingLabelBehavior.always),
// ),
// popupProps: PopupProps.menu(
// isFilterOnline: true,
// searchFieldProps: TextFieldProps(
// cursorColor: AppTheme.primaryColorLight,
// decoration: InputDecoration(
// contentPadding: EdgeInsets.symmetric(
// vertical: ScreenAdaper.height(15),
// horizontal: ScreenAdaper.width(10)))),
// searchDelay: const Duration(milliseconds: 500),
// emptyBuilder: (context, searchEntry) => const Empty(
// text: '未找到',
// ),
// itemBuilder: (_, itemData, _1) => Container(
// decoration: const BoxDecoration(
// border: Border(top: BorderSide(color: AppTheme.dividerColor))),
// padding: EdgeInsets.symmetric(
// vertical: ScreenAdaper.height(5),
// horizontal: ScreenAdaper.width(10)),
// child: Row(
// children: [
// Text(
// itemData.productNumber!,
// style: TextStyle(fontSize: ScreenAdaper.sp(20)),
// ),
// SizedBox(
// width: ScreenAdaper.width(10),
// ),
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// '${itemData.name}',
// style: TextStyle(fontSize: ScreenAdaper.sp(20)),
// ),
// Text(
// '${itemData.company?.name}',
// style: TextStyle(
// fontSize: ScreenAdaper.sp(15), color: AppTheme.grey),
// )
// ],
// )),
// // SizedBox(
// // width: ScreenAdaper.width(10),
// // ),
// // Text(
// // itemData.productNumber!,
// // style: TextStyle(fontSize: ScreenAdaper.sp(20)),
// // ),
// ],
// ),
// ),
// showSearchBox: true,
// ),
// onChanged: print,
// // selectedItem: "Brazil",
// );
// return ZtSearchSelect<ProductModel>(
// isRequired: true,
// textController: controller.productTextController,
// labelText: '产品',
// itemBuilder: (_, itemData) => Container(
// padding: EdgeInsets.symmetric(
// vertical: ScreenAdaper.height(5),
// horizontal: ScreenAdaper.width(10)),
// child: Row(
// children: [
// Text(
// itemData.productNumber!,
// style: TextStyle(fontSize: ScreenAdaper.sp(20)),
// ),
// SizedBox(
// width: ScreenAdaper.width(10),
// ),
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// '${itemData.name}',
// style: TextStyle(fontSize: ScreenAdaper.sp(20)),
// ),
// Text(
// '${itemData.company?.name}',
// style: TextStyle(
// fontSize: ScreenAdaper.sp(15),
// color: AppTheme.grey),
// )
// ],
// )),
// // SizedBox(
// // width: ScreenAdaper.width(10),
// // ),
// // Text(
// // itemData.productNumber!,
// // style: TextStyle(fontSize: ScreenAdaper.sp(20)),
// // ),
// ],
// ),
// ),
// suggestionsCallback: (String keyword) {
// return controller.getProducts(keyword: keyword);
// },
// onClear: () {
// controller.payload.remove('productId');
// },
// onSelected: (ProductModel product) {
// controller.productTextController.text = product.name ?? '';
// controller.payload['productId'] = product.id;
// });
} }
/// ///
@ -205,6 +357,73 @@ class NewInventoryInout extends StatelessWidget {
); );
} }
///
Widget buildPositionBottomPicker() {
final firstLevelData = [
...dictService.getDictItemsByCode(DictTypeEnum.InventoryRoom)
];
final secondLevelData = [
DictItemModel(label: '未知', value: '未知'),
...dictService.getDictItemsByCode(DictTypeEnum.InventoryLine)
];
final thirdDatas =
dictService.getDictItemsByCode(DictTypeEnum.InventoryLineLevel);
final thirdtLevelData = [
if (thirdDatas.isNotEmpty) DictItemModel(label: '未知', value: '未知'),
...thirdDatas
];
return TextFormField(
controller: controller.positionTextController,
decoration: const InputDecoration(
labelText: '库存位置',
floatingLabelBehavior: FloatingLabelBehavior.always),
readOnly: true,
onTap: () {
printInfo(
info: firstLevelData
.map((e) => Center(child: (Text(e.label))))
.toList()
.toString());
ModalUtil.showBottomSheetPicker(
title: '库存位置',
firstLevel: firstLevelData
.map((e) => Center(child: (Text(e.label))))
.toList(),
secondLevel: secondLevelData
.map((e) => Center(child: (Text(e.label))))
.toList(),
thirdLevel: thirdtLevelData
.map((e) => Center(child: (Text(e.label))))
.toList(),
onChanged: (_0, _1, _2) {
String position = firstLevelData[_0].label;
if (_1 >= 1) {
position += ', ${secondLevelData[_1].label}';
}
if (_2 >= 1) {
position += ', ${thirdtLevelData[_2].label}';
}
// , ${secondLevelData[_1].label}, ${thirdtLevelData[_2].label}
controller.positionTextController.text = position;
});
},
onChanged: (_) {
printInfo(info: _);
});
// return ZtTextInput(
// textController: controller.agentTextController,
// labelText: '库存位置',
// isRequired: true,
// onTap: () => ModalUtil.showBottomSheetPicker(
// title: '库存位置',
// firstLevel: dictService
// .getDictItemsByCode(DictTypeEnum.InventoryRoom)
// .map((e) => Text(e.label))
// .toList(),
// onChanged: () {}),
// );
}
/// ///
Widget buildImageUploadQueue() { Widget buildImageUploadQueue() {
return Container( return Container(
@ -217,7 +436,7 @@ class NewInventoryInout extends StatelessWidget {
Text( Text(
'*请拍照上传产品照片', '*请拍照上传产品照片',
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.sp(30), fontSize: ScreenAdaper.sp(25),
color: AppTheme.secondPrimaryColor), color: AppTheme.secondPrimaryColor),
), ),
SizedBox( SizedBox(
@ -247,8 +466,8 @@ class NewInventoryInout extends StatelessWidget {
children: [ children: [
Container( Container(
margin: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)), margin: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)),
width: ScreenAdaper.width(200), width: ScreenAdaper.width(180),
height: ScreenAdaper.width(200), height: ScreenAdaper.width(180),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(width: 1.0, color: AppTheme.dividerColor), border: Border.all(width: 1.0, color: AppTheme.dividerColor),
@ -303,8 +522,8 @@ class NewInventoryInout extends StatelessWidget {
}, },
child: Container( child: Container(
margin: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)), margin: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(5)),
width: ScreenAdaper.width(200), width: ScreenAdaper.width(180),
height: ScreenAdaper.width(200), height: ScreenAdaper.width(180),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all(width: 1.0, color: AppTheme.dividerColor), border: Border.all(width: 1.0, color: AppTheme.dividerColor),

View File

@ -44,6 +44,7 @@ class NewInventoryInoutController extends GetxController {
final quantityTextController = TextEditingController(); final quantityTextController = TextEditingController();
final agentTextController = TextEditingController(); final agentTextController = TextEditingController();
final remarkTextController = TextEditingController(); final remarkTextController = TextEditingController();
final positionTextController = TextEditingController();
final uploadImgFilesPath = <String>[].obs; final uploadImgFilesPath = <String>[].obs;
final uploadScrollController = ScrollController(); final uploadScrollController = ScrollController();
Map<String, dynamic> payload = {}; Map<String, dynamic> payload = {};
@ -60,7 +61,7 @@ class NewInventoryInoutController extends GetxController {
Future<List<ProductModel>> getProducts({String? keyword}) async { Future<List<ProductModel>> getProducts({String? keyword}) async {
final res = final res =
await Api.getProducts({'page': 1, 'pageSize': 10, 'name': keyword}); await Api.getProducts({'page': 1, 'pageSize': 10, 'keyword': keyword});
if (res.data != null) { if (res.data != null) {
products.assignAll( products.assignAll(
res.data!.items.map((e) => ProductModel.fromJson(e)).toList()); res.data!.items.map((e) => ProductModel.fromJson(e)).toList());

View File

@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/api.dart' as Api; import 'package:sk_base_mobile/apis/api.dart' as Api;
import 'package:sk_base_mobile/util/logger_util.dart'; import 'package:sk_base_mobile/util/logger_util.dart';
import 'package:sk_base_mobile/widgets/tap_to_dismiss_keyboard.dart'; import 'package:sk_base_mobile/widgets/tap_to_dismiss_keyboard.dart';
import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/models/auth.dart'; import 'package:sk_base_mobile/models/auth.dart';
import 'package:sk_base_mobile/models/user_info.model.dart'; import 'package:sk_base_mobile/models/user_info.model.dart';
import 'package:sk_base_mobile/services/service.dart'; import 'package:sk_base_mobile/services/service.dart';

40
lib/store/dict.store.dart Normal file
View File

@ -0,0 +1,40 @@
import 'package:get/get.dart';
import 'package:get/get_state_manager/get_state_manager.dart';
import 'package:sk_base_mobile/constants/dict_enum.dart';
import 'package:sk_base_mobile/models/dict_item.model.dart';
import 'package:sk_base_mobile/models/dict_type.model.dart';
import 'package:sk_base_mobile/apis/api.dart' as Api;
import 'package:sk_base_mobile/util/logger_util.dart';
const needCachedKey = [
DictTypeEnum.InventoryRoom,
DictTypeEnum.InventoryLine,
DictTypeEnum.InventoryLineLevel
];
class DictService extends GetxService {
Future<DictService> init() async {
await getDictTypes();
return this;
}
RxList<DictTypeModel> dictTypes = RxList([]);
Future<void> getDictTypes() async {
try {
final response = await Api.getDictTypeAll({
'storeCodes': needCachedKey,
'withItems': true,
});
dictTypes.value = (response.data as List)
.map((item) => DictTypeModel.fromJson(item))
.toList();
} catch (e) {
LoggerUtil().error('getDictTypes error: $e');
}
}
List<DictItemModel> getDictItemsByCode(String code) {
return dictTypes.firstWhereOrNull((item) => item.code == code)?.dictItems ??
[];
}
}

View File

@ -1,6 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/widgets/core/zt_bottomsheet_picker.dart';
class ModalUtil { class ModalUtil {
static Future<bool> showWarningDialog( static Future<bool> showWarningDialog(
@ -40,4 +42,27 @@ class ModalUtil {
); );
return confirmed; return confirmed;
} }
/// 3
static Future<void> showBottomSheetPicker(
{title = '请选择',
required onChanged,
required firstLevel,
secondLevel,
thirdLevel,
itemHeight,
popupHeight}) async =>
showCupertinoModalPopup(
context: Get.overlayContext!,
builder: (BuildContext context) {
return ZtBottomSheetPicker(
title: title,
onChanged: onChanged,
firstLevel: firstLevel,
secondLevel: secondLevel,
thirdLevel: thirdLevel,
itemHeight: itemHeight ?? 80.0,
popupHeight: popupHeight ?? 400);
},
);
} }

View File

@ -0,0 +1,101 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sk_base_mobile/util/logger_util.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
class ZtBaseDatePicker extends StatelessWidget {
final Function? onDateTimeChanged;
final yearController =
FixedExtentScrollController(initialItem: DateTime.now().year - 2000);
final monthController =
FixedExtentScrollController(initialItem: DateTime.now().month - 1);
final dayController =
FixedExtentScrollController(initialItem: DateTime.now().day - 1);
ZtBaseDatePicker({super.key, this.onDateTimeChanged});
@override
Widget build(BuildContext context) {
return Container(
height: ScreenAdaper.height(250),
color: Colors.white,
child: Column(
children: [
Row(
children: [
TextButton(
onPressed: () {
Get.back();
},
child: Text(
'取消',
style: TextStyle(fontSize: ScreenAdaper.sp(25)),
)),
const Spacer(),
TextButton(
onPressed: () {
Get.back();
final year = 2000 + yearController.selectedItem;
String month = '${monthController.selectedItem + 1}';
if (int.parse(month) < 10) {
month = '0$month';
}
final day = dayController.selectedItem + 1;
if (onDateTimeChanged != null) {
onDateTimeChanged!('$year-$month-$day');
}
LoggerUtil().info('$year-$month-$day');
},
child: Text(
'确定',
style: TextStyle(fontSize: ScreenAdaper.sp(25)),
))
],
),
Expanded(
child: Row(
children: <Widget>[
Expanded(
child: CupertinoPicker(
scrollController: yearController,
itemExtent: ScreenAdaper.height(60),
onSelectedItemChanged: (int index) {
//
},
children: List<Widget>.generate(DateTime.now().year - 1999,
(int index) {
return Center(child: Text('${2000 + index}'));
}),
),
),
Expanded(
child: CupertinoPicker(
scrollController: monthController,
itemExtent: ScreenAdaper.height(60),
onSelectedItemChanged: (int index) {
//
},
children: List<Widget>.generate(12, (int index) {
return Center(child: Text('${index + 1}'));
}),
),
),
Expanded(
child: CupertinoPicker(
scrollController: dayController,
itemExtent: ScreenAdaper.height(60),
onSelectedItemChanged: (int index) {
//
},
children: List<Widget>.generate(31, (int index) {
return Center(child: Text('${index + 1}'));
}),
),
),
],
))
],
),
);
}
}

View File

@ -0,0 +1,112 @@
import 'package:flutter/cupertino.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';
class ZtBottomSheetPicker extends StatelessWidget {
final Function? onChanged;
FixedExtentScrollController firstLevelControlller =
FixedExtentScrollController();
FixedExtentScrollController secondLevelController =
FixedExtentScrollController();
FixedExtentScrollController thirdLevelController =
FixedExtentScrollController();
final List<Widget> firstLevel;
final List<Widget>? secondLevel;
final List<Widget>? thirdLevel;
double? itemHeight;
double? popupHeight;
final String title;
ZtBottomSheetPicker(
{super.key,
this.title = '请选择',
required this.onChanged,
required this.firstLevel,
this.secondLevel,
this.thirdLevel,
this.itemHeight = 80.0,
this.popupHeight = 400.0});
@override
Widget build(BuildContext context) {
return Container(
height: ScreenAdaper.height(popupHeight!),
color: Colors.white,
child: Column(
children: [
Row(
children: [
TextButton(
onPressed: () {
Get.back();
},
child: Text(
'取消',
style: TextStyle(fontSize: ScreenAdaper.sp(25)),
)),
Expanded(
child: Center(
child: Text(
title,
style: TextStyle(
color: AppTheme.nearlyBlack,
fontSize: ScreenAdaper.sp(25),
decoration: TextDecoration.none),
),
)),
TextButton(
onPressed: () {
Get.back();
onChanged!(
firstLevelControlller.selectedItem,
(secondLevel ?? []).isNotEmpty
? secondLevelController.selectedItem
: -1,
(thirdLevel ?? []).isNotEmpty
? thirdLevelController.selectedItem
: -1,
);
},
child: Text(
'确定',
style: TextStyle(fontSize: ScreenAdaper.sp(25)),
))
],
),
Expanded(
child: Row(
children: <Widget>[
Expanded(
child: CupertinoPicker(
scrollController: firstLevelControlller,
itemExtent: ScreenAdaper.height(itemHeight!),
onSelectedItemChanged: (int index) {},
children: firstLevel,
),
),
if ((secondLevel ?? []).isNotEmpty)
Expanded(
child: CupertinoPicker(
scrollController: secondLevelController,
itemExtent: ScreenAdaper.height(itemHeight!),
onSelectedItemChanged: (int index) {},
children: secondLevel!,
),
),
if ((thirdLevel ?? []).isNotEmpty)
Expanded(
child: CupertinoPicker(
scrollController: thirdLevelController,
itemExtent: ScreenAdaper.height(itemHeight!),
onSelectedItemChanged: (int index) {},
children: thirdLevel!,
),
),
],
))
],
),
);
}
}

View File

@ -4,14 +4,14 @@ import 'package:get/get.dart';
import 'package:sk_base_mobile/app_theme.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/screen_adaper_util.dart';
class ZtSearchSelect extends StatelessWidget { class ZtSearchSelect<T> extends StatelessWidget {
final TextEditingController textController; final TextEditingController textController;
final Function? suggestionsCallback; final Function? suggestionsCallback;
final Function? onSelected; final void Function(T)? onSelected;
final VoidCallback? onClear; final VoidCallback? onClear;
final String labelText; final String labelText;
final bool isRequired; final bool isRequired;
final Widget Function(dynamic itemData)? itemBuilder; final Widget Function(BuildContext, T) itemBuilder;
const ZtSearchSelect( const ZtSearchSelect(
{super.key, {super.key,
required this.textController, required this.textController,
@ -89,13 +89,13 @@ class ZtSearchSelect extends StatelessWidget {
), ),
), ),
itemBuilder: (context, itemData) { itemBuilder: (context, itemData) {
return itemBuilder!(itemData); return itemBuilder(context, itemData as T);
}, },
onSelected: (selectedItemData) { onSelected: (selectedItemData) {
// //
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
if (onSelected != null) { if (onSelected != null) {
onSelected!(selectedItemData); onSelected!(selectedItemData as T);
} }
}); });
} }

View File

@ -12,14 +12,15 @@ class ZtTextInput extends StatelessWidget {
final String labelText; final String labelText;
final String? hint; final String? hint;
final bool isTextArea; final bool isTextArea;
const ZtTextInput( const ZtTextInput({
{super.key, super.key,
required this.textController, required this.textController,
this.onTap, this.onTap,
this.hint, this.hint,
this.isRequired = false, this.isRequired = false,
this.labelText = '', this.labelText = '',
this.isTextArea = false}); this.isTextArea = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFormField( return TextFormField(
@ -27,7 +28,7 @@ class ZtTextInput extends StatelessWidget {
onTapOutside: (event) { onTapOutside: (event) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
maxLines: isTextArea ? 3 : 1, // maxLines: isTextArea ? 2 : 1, //
onTap: onTap ?? () {}, onTap: onTap ?? () {},
decoration: InputDecoration( decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,

View File

@ -10,10 +10,10 @@ class Empty extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Center( // Center(
child: Image( // child: Image(
height: ScreenAdaper.height(130), // height: ScreenAdaper.height(130),
image: const AssetImage('assets/images/empty_icon.png'))), // image: const AssetImage('assets/images/empty_icon.png'))),
Text( Text(
text ?? '', text ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@ -0,0 +1,17 @@
import 'package:flutter/cupertino.dart';
import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
class LoadingIndicator extends StatelessWidget {
final bool animating;
const LoadingIndicator({super.key, this.animating = true});
@override
Widget build(BuildContext context) {
return CupertinoActivityIndicator(
animating: animating,
color: AppTheme.primaryColor,
radius: ScreenAdaper.sp(20),
);
}
}

View File

@ -1,6 +1,9 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.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/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/loading_indicator.dart';
class RefreshFooter extends StatefulWidget { class RefreshFooter extends StatefulWidget {
const RefreshFooter({Key? key}) : super(key: key); const RefreshFooter({Key? key}) : super(key: key);
@ -32,16 +35,16 @@ class _RefreshFooterState extends State<RefreshFooter>
builder: (BuildContext context, LoadStatus? status) { builder: (BuildContext context, LoadStatus? status) {
Widget body; Widget body;
if (status == LoadStatus.idle) { if (status == LoadStatus.idle) {
body = const Text("Pull up to get latest messages"); body = const Text("上拉获取更多");
} else if (status == LoadStatus.loading) { } else if (status == LoadStatus.loading) {
body = const SizedBox(child: const CupertinoActivityIndicator()); body = const LoadingIndicator();
} else if (status == LoadStatus.failed) { } else if (status == LoadStatus.failed) {
body = Text( body = Text(
"Load failed, please try again", "获取失败,请重试",
style: style, style: style,
); );
} else if (status == LoadStatus.canLoading) { } else if (status == LoadStatus.canLoading) {
body = const CupertinoActivityIndicator( body = const LoadingIndicator(
animating: false, animating: false,
); );
} else { } else {

View File

@ -1,6 +1,9 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.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/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/loading_indicator.dart';
class RefreshHeader extends RefreshIndicator { class RefreshHeader extends RefreshIndicator {
RefreshHeader() RefreshHeader()
@ -39,15 +42,17 @@ class RefreshHeaderState extends RefreshIndicatorState<RefreshHeader>
TextStyle style = TextStyle(fontSize: ScreenAdaper.sp(18)); TextStyle style = TextStyle(fontSize: ScreenAdaper.sp(18));
Widget body; Widget body;
if (mode == RefreshStatus.refreshing) { if (mode == RefreshStatus.refreshing) {
body = const CupertinoActivityIndicator(); body = const LoadingIndicator();
} else if (mode == RefreshStatus.canRefresh) { } else if (mode == RefreshStatus.canRefresh) {
body = const CupertinoActivityIndicator(animating: false); body = const LoadingIndicator(
animating: false,
);
} else if (mode == RefreshStatus.completed) { } else if (mode == RefreshStatus.completed) {
body = const SizedBox(); body = const SizedBox();
} else if (mode == RefreshStatus.failed) { } else if (mode == RefreshStatus.failed) {
body = Text("No more Data)", style: style); body = Text("没有更多数据了", style: style);
} else { } else {
body = const CupertinoActivityIndicator(); body = const LoadingIndicator();
} }
return SizedBox( return SizedBox(
height: ScreenAdaper.height(40), height: ScreenAdaper.height(40),

View File

@ -177,6 +177,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.1" version: "5.4.1"
dropdown_search:
dependency: "direct main"
description:
name: dropdown_search
sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:

View File

@ -57,6 +57,7 @@ dependencies:
path: ^1.8.3 path: ^1.8.3
path_provider: ^2.1.2 path_provider: ^2.1.2
flutter_typeahead: ^5.2.0 flutter_typeahead: ^5.2.0
dropdown_search: ^5.0.6
dev_dependencies: dev_dependencies: