feat: cascade picker common widget developed

This commit is contained in:
louis 2024-04-09 11:44:55 +08:00
parent cee31e6896
commit eabb746738
22 changed files with 502 additions and 480 deletions

View File

@ -15,6 +15,11 @@ class Api {
); );
} }
///
static Future<Response> getDepts() {
return DioService.dio.get(Urls.depts);
}
/// By key /// By key
static Future<Response> getSystemParamConfigByCode(String code) { static Future<Response> getSystemParamConfigByCode(String code) {
return DioService.dio.get( return DioService.dio.get(

View File

@ -16,4 +16,5 @@ class Urls {
static String systemParamConfig = 'system/param-config'; static String systemParamConfig = 'system/param-config';
static String accountMenus = 'account/menus'; static String accountMenus = 'account/menus';
static String userInfo = 'system/users'; static String userInfo = 'system/users';
static String depts = 'system/depts';
} }

View File

@ -1,25 +1,37 @@
class DeptModel { import 'package:sk_base_mobile/util/common.util.dart';
class DeptModel extends TreeNode<DeptModel> {
DeptModel({ DeptModel({
required this.id, required this.id,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.name, required this.name,
required this.orderNo, required this.orderNo,
this.parent,
this.children,
}); });
final int? id; final int id;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final String? name; final String name;
final int? orderNo; final int orderNo;
final DeptModel? parent;
List<DeptModel>? children = [];
factory DeptModel.fromJson(Map<String, dynamic> json) { factory DeptModel.fromJson(Map<String, dynamic> json) {
return DeptModel( return DeptModel(
id: json["id"], id: json["id"] ?? 0,
createdAt: DateTime.tryParse(json["createdAt"] ?? ""), createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""), updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
name: json["name"], name: json["name"] ?? "",
orderNo: json["orderNo"], orderNo: json["orderNo"] ?? 0,
parent:
json["parent"] == null ? null : DeptModel.fromJson(json["parent"]),
children: json["children"] == null
? []
: List<DeptModel>.from(
json["children"]!.map((x) => DeptModel.fromJson(x))),
); );
} }
@ -29,5 +41,7 @@ class DeptModel {
"updatedAt": updatedAt?.toIso8601String(), "updatedAt": updatedAt?.toIso8601String(),
"name": name, "name": name,
"orderNo": orderNo, "orderNo": orderNo,
"parent": parent?.toJson(),
"children": children?.map((x) => x.toJson()).toList(),
}; };
} }

View File

@ -1,90 +1,63 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; 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/app_theme.dart';
import 'package:sk_base_mobile/models/dept.model.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/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/core/sk_cascade_picker.dart'; import 'package:sk_base_mobile/widgets/core/sk_cascade_picker.dart';
import 'package:sk_base_mobile/widgets/gradient_button.dart';
class DeptPicker extends StatelessWidget { class DeptPicker extends StatelessWidget {
DeptPicker({super.key}); final Function(String)? onSelected;
DeptPicker({super.key, this.onSelected});
final _cascadeController = Get.put(CascadeController());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SkCascadePicker<int, DeptModel>(
decoration: BoxDecoration(color: AppTheme.nearlyWhite), getData: getData,
height: ScreenAdaper.height(400), initialPageData: (cascadeController) =>
child: Column( cascadeController.treeData.toList(),
children: [ nextPageData: (pageCallback, currentPage, selectIndex, currentPageItem,
GradientButton( cascadeController) async {
buttonText: '确定', if (currentPageItem.children != null) {
onPressed: () { List<CascadeItem<int, DeptModel>>? nextPageData = currentPageItem
if (_cascadeController.isCompleted()) { .children!
// titles .map<CascadeItem<int, DeptModel>>((e) =>
List<String> selectedTitles = CascadeItem(label: e.name, value: e.id, children: e.children))
_cascadeController.selectedTitles; .toList();
print("已选中的titles: $selectedTitles"); if (nextPageData.isNotEmpty) pageCallback(nextPageData);
// }
List<int> selectedIndexes = },
_cascadeController.selectedIndexes; onConfirm: (List<CascadeItem<int, DeptModel>> value) {
print("已选中的序号:$selectedIndexes"); if (onSelected != null && value.isNotEmpty) {
onSelected!(value.last.label);
}
},
maxPageNum: 10,
tabTitleStyle:
TextStyle(fontSize: ScreenAdaper.height(26), color: Colors.black),
itemTitleStyle:
TextStyle(fontSize: ScreenAdaper.height(26), color: Colors.black),
selectedIcon: const Icon(Icons.check, color: AppTheme.primaryColorLight),
);
}
Item item = _cascadeController Future<List<CascadeItem<int, DeptModel>>> getData() async {
.items[selectedIndexes[0]] await Future.delayed(const Duration(milliseconds: 500));
.children![selectedIndexes[1]] try {
.children![selectedIndexes[2]]; final res = await Api.getDepts();
print("已选择item( ${item.name} )"); if (res.data != null) {
} List<CascadeItem<int, DeptModel>> result =
}, res.data.map<CascadeItem<int, DeptModel>>((e) {
), DeptModel data = DeptModel.fromJson(e);
SizedBox( return CascadeItem(
height: 10, label: data.name, value: data.id, children: data.children);
), }).toList();
Expanded( return result;
child: CascadePicker( }
initialPageData: return [];
_cascadeController.items.map((e) => e.name!).toList(), } catch (e) {
nextPageData: (pageCallback, currentPage, selectIndex) async { LoggerUtil().error(e);
print("当前选择: 第$currentPage页, 第$selectIndex项"); return [];
if (currentPage == 1) { }
//
List<String>? nextPageData = _cascadeController
.items[selectIndex].children
?.map((e) => e.name!)
.toList();
if (nextPageData != null) pageCallback(nextPageData);
} else if (currentPage == 2) {
//
//
List<int> selectedIndexes =
_cascadeController.selectedIndexes;
// items中获取下一级页面的列表数据
List<String>? nextPageData = _cascadeController
.items[selectedIndexes[0]].children?[selectIndex].children
?.map((e) => e.name!)
.toList();
if (nextPageData != null) pageCallback(nextPageData);
}
},
controller: _cascadeController,
maxPageNum: 3,
tabTitleStyle: TextStyle(
fontSize: ScreenAdaper.height(26), color: Colors.black),
itemTitleStyle: TextStyle(
fontSize: ScreenAdaper.height(26), color: Colors.black),
selectedIcon:
Icon(Icons.check, color: AppTheme.primaryColorLight),
))
],
));
} }
} }
class Item {
String? name;
String? code;
String? fatherCode;
String? remark;
List<Item>? children;
}

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart'; import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/user_info.model.dart'; import 'package:sk_base_mobile/models/user_info.model.dart';
@ -46,6 +46,8 @@ class EditUserInfo extends StatelessWidget {
SizedBox( SizedBox(
height: ScreenAdaper.height(defaultPadding), height: ScreenAdaper.height(defaultPadding),
), ),
///
SkTextInput( SkTextInput(
isDense: true, isDense: true,
textController: controller.nameEditController, textController: controller.nameEditController,
@ -54,19 +56,24 @@ class EditUserInfo extends StatelessWidget {
SizedBox( SizedBox(
height: ScreenAdaper.height(defaultPadding), height: ScreenAdaper.height(defaultPadding),
), ),
///
SkTextInput( SkTextInput(
isDense: true, isDense: true,
keyboardType: TextInputType.none, keyboardType: TextInputType.none,
textController: TextEditingController(), textController: controller.deptEditController,
labelText: '所属部门', labelText: '所属部门',
onTap: (_) async { onTap: (_) async {
Get.bottomSheet(DeptPicker()) Get.bottomSheet(DeptPicker(onSelected: (String label) {
.then((value) => Get.delete<CascadeController>()); controller.deptEditController.text = label;
}));
}, },
), ),
SizedBox( SizedBox(
height: ScreenAdaper.height(defaultPadding), height: ScreenAdaper.height(defaultPadding),
), ),
///
SkTextInput( SkTextInput(
isDense: true, isDense: true,
keyboardType: TextInputType.none, keyboardType: TextInputType.none,
@ -75,7 +82,8 @@ class EditUserInfo extends StatelessWidget {
onTap: (_) async { onTap: (_) async {
Get.bottomSheet(Container( Get.bottomSheet(Container(
height: ScreenAdaper.height(400), height: ScreenAdaper.height(400),
decoration: BoxDecoration(color: AppTheme.nearlyWhite), decoration:
const BoxDecoration(color: AppTheme.nearlyWhite),
child: Column(children: [ child: Column(children: [
Container( Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
@ -88,7 +96,7 @@ class EditUserInfo extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: ScreenAdaper.height(30)), fontSize: ScreenAdaper.height(30)),
), ),
Spacer(), const Spacer(),
Text( Text(
'确定', '确定',
style: TextStyle( style: TextStyle(
@ -262,6 +270,7 @@ class EditUserInfoController extends GetxController {
int userId; int userId;
EditUserInfoController(this.userId); EditUserInfoController(this.userId);
final nameEditController = TextEditingController(); final nameEditController = TextEditingController();
final deptEditController = TextEditingController();
final userInfo = Rxn<UserInfoModel>(); final userInfo = Rxn<UserInfoModel>();
RxList selectedDepts = RxList([]); RxList selectedDepts = RxList([]);

View File

@ -130,7 +130,7 @@ class EmployeeDetail extends StatelessWidget {
text: '考勤记录', text: '考勤记录',
), ),
Tab( Tab(
text: '工作安排', text: '任务进度',
), ),
Tab( Tab(
text: '相关文件', text: '相关文件',

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/constants/bg_color.dart'; import 'package:sk_base_mobile/constants/bg_color.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/constants/bg_color.dart'; import 'package:sk_base_mobile/constants/bg_color.dart';

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/enum.dart'; import 'package:sk_base_mobile/constants/enum.dart';
import 'package:sk_base_mobile/db_helper/db_help.dart'; import 'package:sk_base_mobile/db_helper/db_help.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/models/app_bottom_nav_item.dart'; import 'package:sk_base_mobile/models/app_bottom_nav_item.dart';
import 'package:sk_base_mobile/screens/inventory/inventory.dart'; import 'package:sk_base_mobile/screens/inventory/inventory.dart';
import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout.dart'; import 'package:sk_base_mobile/screens/inventory_inout/inventory_inout.dart';

View File

@ -1,6 +1,6 @@
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/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/util/device.util.dart'; import 'package:sk_base_mobile/util/device.util.dart';
import 'package:sk_base_mobile/util/snack_bar.util.dart'; import 'package:sk_base_mobile/util/snack_bar.util.dart';
import '../../constants/constants.dart'; import '../../constants/constants.dart';

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart'; import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/user_info.model.dart'; import 'package:sk_base_mobile/models/user_info.model.dart';

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart'; import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/index.dart'; import 'package:sk_base_mobile/models/index.dart';

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/constants/bg_color.dart'; import 'package:sk_base_mobile/constants/bg_color.dart';
import 'package:sk_base_mobile/models/index.dart'; import 'package:sk_base_mobile/models/index.dart';

View File

@ -4,7 +4,7 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/constants/enum.dart'; import 'package:sk_base_mobile/constants/enum.dart';
import 'package:sk_base_mobile/db_helper/db_help.dart'; import 'package:sk_base_mobile/db_helper/db_help.dart';
import 'package:sk_base_mobile/models/index.dart'; import 'package:sk_base_mobile/models/index.dart';

View File

@ -7,7 +7,7 @@ import 'package:get/get.dart';
import 'package:install_plugin/install_plugin.dart'; import 'package:install_plugin/install_plugin.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/app_theme.dart'; import 'package:sk_base_mobile/app_theme.dart';
import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/models/app_config.dart'; import 'package:sk_base_mobile/models/app_config.dart';

View File

@ -118,10 +118,10 @@ class DioService extends get_package.GetxService {
options.headers['model'] = StorageService.to options.headers['model'] = StorageService.to
.getString(CacheKeys.deviceModel, isWithUser: false); // .getString(CacheKeys.deviceModel, isWithUser: false); //
} }
if (GloablConfig.DEBUG && (options.data is! FormData)) { // if (GloablConfig.DEBUG && (options.data is! FormData)) {
LoggerUtil().info( // LoggerUtil().info(
'[Service-dio] url: ${options.path}, params: ${jsonEncode(options.queryParameters)}, body: ${jsonEncode(options.data)}, Header:${jsonEncode(options.headers)}'); // '[Service-dio] url: ${options.path}, params: ${jsonEncode(options.queryParameters)}, body: ${jsonEncode(options.data)}, Header:${jsonEncode(options.headers)}');
} // }
handler.next(options); handler.next(options);
} }
@ -135,7 +135,7 @@ class DioService extends get_package.GetxService {
} }
if (response.data != null && response.data is Map) { if (response.data != null && response.data is Map) {
if (response.data['code'] == 200) { if (response.data['code'] == 200) {
if (GloablConfig.DEBUG) LoggerUtil().info(response.data['data']); // if (GloablConfig.DEBUG) LoggerUtil().info(response.data['data']);
response.data = response.data['data']; response.data = response.data['data'];
// //
if (response.data != null && if (response.data != null &&

View File

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:dio/dio.dart' as dio_package; import 'package:dio/dio.dart' as dio_package;
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/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/store/dict.store.dart'; import 'package:sk_base_mobile/store/dict.store.dart';
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';

View File

@ -2,7 +2,7 @@ import 'package:get/get.dart';
import 'package:sk_base_mobile/constants/dict_enum.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_item.model.dart';
import 'package:sk_base_mobile/models/dict_type.model.dart'; import 'package:sk_base_mobile/models/dict_type.model.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:sk_base_mobile/store/auth.store.dart'; import 'package:sk_base_mobile/store/auth.store.dart';
import 'package:sk_base_mobile/util/logger_util.dart'; import 'package:sk_base_mobile/util/logger_util.dart';

View File

@ -26,4 +26,18 @@ class CommonUtil {
static String firstUppercase(String text) { static String firstUppercase(String text) {
return '${text[0].toUpperCase()}${text.substring(1)}'; return '${text[0].toUpperCase()}${text.substring(1)}';
} }
static List<T> flattenTree<T extends TreeNode>(List<T> children) {
List<T> result = children;
for (T child in children) {
if (child.children != null) {
result.addAll(flattenTree(child.children as List<T>));
}
}
return result;
}
}
class TreeNode<T> {
List<T>? children;
} }

View File

@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'package:dio/dio.dart' as dio_package; import 'package:dio/dio.dart' as dio_package;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:sk_base_mobile/apis/index.dart'; import 'package:sk_base_mobile/apis/api.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:sk_base_mobile/config.dart'; import 'package:sk_base_mobile/config.dart';
import 'package:sk_base_mobile/models/upload_result.model.dart'; import 'package:sk_base_mobile/models/upload_result.model.dart';

View File

@ -1,59 +1,21 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/screens/hr_manage/components/dept_picker.dart'; import 'package:sk_base_mobile/util/screen_adaper_util.dart';
import 'package:sk_base_mobile/widgets/gradient_button.dart';
import 'package:sk_base_mobile/widgets/loading_indicator.dart';
/// class CascadeItem<T, Z> {
/// 使: late String label;
/// ```dart T? value;
/// CascadePicker的page是ListView List<Z>? children;
/// CascadeItem({required this.label, this.value, this.children});
/// }
/// final _cascadeController = CascadeController();
///
/// initialPageData:
/// nextPageData:
/// - pageCallback: CascadePicker
/// - currentPage:
/// - selectIndex:
/// controller:
/// maxPageNum:
/// selectedIcon: flutter package不能放本地资源文件images文件夹下面
///
/// Expand(
/// child: CascadePicker(
/// initialPageData: ['a', 'b', 'c', 'd'],
/// nextPageData: (pageCallback, currentPage, selectIndex) async {
/// pageCallback(['one', 'two', 'three'])
/// },
/// controller: _cascadeController,
/// maxPageNum: 4,
/// selectedIcon: Image.asset("images/ic_select_mark.png", width: 10, height: 10, color: Colors.redAccent,),
/// )
///
/// InkBox(
/// child: Container(...)
/// onTap: () {
/// ///
/// if (_cascadeController.isCompleted()) {
/// List<String> selectedTitles = _cascadeController.selectedTitles;
/// List<int> selectedIndexes = _cascadeController.selectedIndexes;
/// }
/// }
/// )
/// ```
/// pageData: class SkCascadePicker<T, Z> extends StatelessWidget {
/// currentPage: , final SkCascadePickerController cascadeController;
/// selectIndex:
typedef void NextPageCallback(
Function(List<String>) pageData, int currentPage, int selectIndex);
class CascadePicker extends StatefulWidget {
final List<String> initialPageData;
final NextPageCallback nextPageData;
final int maxPageNum; final int maxPageNum;
final CascadeController controller;
final Color tabColor; final Color tabColor;
final double tabHeight; final double tabHeight;
final TextStyle tabTitleStyle; final TextStyle tabTitleStyle;
@ -62,12 +24,15 @@ class CascadePicker extends StatefulWidget {
final Color itemColor; final Color itemColor;
final Color activeColor; final Color activeColor;
final Widget? selectedIcon; final Widget? selectedIcon;
final Function(List<CascadeItem<T, Z>>)? onConfirm;
CascadePicker( SkCascadePicker(
{required this.initialPageData, {required Future<List<CascadeItem<T, Z>>> Function() getData,
required this.nextPageData, required List<CascadeItem<T, Z>> Function(SkCascadePickerController<T, Z>)
initialPageData,
required NextPageCallback<T, Z> nextPageData,
this.onConfirm,
this.maxPageNum = 3, this.maxPageNum = 3,
required this.controller,
this.tabHeight = 40, this.tabHeight = 40,
this.activeColor = AppTheme.primaryColor, this.activeColor = AppTheme.primaryColor,
this.tabColor = Colors.white, this.tabColor = Colors.white,
@ -75,394 +40,435 @@ class CascadePicker extends StatefulWidget {
this.itemHeight = 40, this.itemHeight = 40,
this.itemColor = Colors.white, this.itemColor = Colors.white,
this.itemTitleStyle = const TextStyle(color: Colors.black, fontSize: 14), this.itemTitleStyle = const TextStyle(color: Colors.black, fontSize: 14),
this.selectedIcon}); this.selectedIcon})
: cascadeController = Get.put(SkCascadePickerController<T, Z>(
initialPageData: initialPageData,
nextPageData: nextPageData,
getData: getData));
@override @override
_CascadePickerState createState() => _CascadePickerState(this.controller); Widget build(BuildContext context) {
} return Container(
decoration: const BoxDecoration(color: AppTheme.nearlyWhite),
height: ScreenAdaper.height(400),
width: Get.width,
child: Obx(() => cascadeController.loading.value
? const LoadingIndicator(
common: true,
)
: Column(children: [
GradientButton(
buttonText: '确定',
onPressed: () {
Get.back();
if (onConfirm != null) {
onConfirm!(cascadeController.selectedTabs
.where((e) =>
e.label != SkCascadePickerController.newTabName)
.toList() as List<CascadeItem<T, Z>>);
}
class _CascadePickerState extends State<CascadePicker> // titles
with SingleTickerProviderStateMixin { // List<CascadeItem<T,Z>> selectedTitles =
static String _newTabName = "请选择"; // cascadeController.selectedTabs;
// print("已选中的titles: $selectedTitles");
final CascadeController _cascadeController; //
// List<int> selectedIndexes =
_CascadePickerState(this._cascadeController) { // cascadeController.selectedIndexes;
_cascadeController._setState(this); // print("已选中的序号:$selectedIndexes");
} },
),
late final AnimationController _controller; Expanded(
late final CurvedAnimation _curvedAnimation; child: Column(
Animation? _sliderAnimation; crossAxisAlignment: CrossAxisAlignment.start,
final _sliderFixMargin = ValueNotifier(0.0); children: [
double _sliderWidth = 20; AnimatedBuilder(
animation: cascadeController.sliderAnimation!,
PageController _pageController = PageController(initialPage: 0); builder: (context, child) => Stack(
clipBehavior: Clip.hardEdge,
GlobalKey _sliderKey = GlobalKey(); alignment: Alignment.bottomLeft,
List<GlobalKey> _tabKeys = []; children: [
Container(
/// padding: EdgeInsets.symmetric(
List<List<String>> _pagesData = []; horizontal: ScreenAdaper.width(20)),
width: MediaQuery.of(context).size.width,
/// title集合 child: Obx(
List<String> _selectedTabs = [_newTabName]; () => SingleChildScrollView(
controller: cascadeController.scrollController,
/// item index集合 scrollDirection: Axis.horizontal,
List<int> _selectedIndexes = [-1]; child: Row(children: _tabWidgets()),
),
/// "请选择"tab宽度tab时用到 ),
double _animTabWidth = 0; ),
const Divider(
/// tab添加事件记录"请选择"tab初始化状态 height: 1,
bool _isAddTabEvent = false; ),
ValueListenableBuilder<double>(
/// tab移动未开始'请选择'tab时隐藏文本tab在终点位置 valueListenable: cascadeController.sliderFixMargin,
bool _isAnimateTextHide = false; builder: (_, margin, __) => Positioned(
left: margin +
/// _moveSlider重复调用 cascadeController.sliderAnimation!.value,
bool _isClickAndMoveTab = false; child: Container(
key: cascadeController.sliderKey,
/// width: cascadeController.sliderWidth,
int _currentSelectPage = 0; height: 2,
decoration: BoxDecoration(
_addTab(int page, int atIndex, String currentPageItem) { color: activeColor,
_loadNextPageData(page, atIndex, currentPageItem); borderRadius: BorderRadius.circular(2)),
} ),
),
_loadNextPageData(int page, int atIndex, String currentPageItem, )
{bool isUpdatePage = false}) { ],
widget.nextPageData((data) { ),
final nextPageDataIsEmpty = data.isEmpty; ),
if (!nextPageDataIsEmpty) { Expanded(
/// child: PageView.builder(
setState(() { itemCount: cascadeController.pagesData.length,
if (isUpdatePage) { controller: cascadeController.pageController,
/// itemBuilder: (context, index) => _pageWidget(index),
_pagesData[page] = data; onPageChanged: (position) {
_selectedTabs[page] = _newTabName; if (!cascadeController.isClickAndMoveTab) {
_selectedIndexes[page] = -1; cascadeController.moveSlider(position,
movePage: false);
/// tab数据 }
_pagesData.removeRange(page + 1, _pagesData.length); if (cascadeController.urrentSelectPage == position) {
_selectedIndexes.removeRange(page + 1, _selectedIndexes.length); cascadeController.isClickAndMoveTab = false;
_selectedTabs.removeRange(page + 1, _selectedTabs.length); }
} else { },
/// ),
_isAnimateTextHide = true; )
_isAddTabEvent = true; ],
_pagesData.add(data); ))
_selectedTabs.add(_newTabName); ])));
_selectedIndexes.add(-1);
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_moveSlider(page, isAdd: true);
});
});
} else {
///
final currentPage = page - 1;
setState(() {
_selectedTabs[currentPage] = currentPageItem;
_selectedIndexes[currentPage] = atIndex;
/// tab数据
_pagesData.removeRange(page, _pagesData.length);
_selectedIndexes.removeRange(page, _selectedIndexes.length);
_selectedTabs.removeRange(page, _selectedTabs.length);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//
_moveSlider(currentPage);
});
});
}
}, page, atIndex);
}
_moveSlider(int page, {bool movePage = true, bool isAdd = false}) {
if (movePage && _currentSelectPage != page) {
/// tab标签
/// _isClickAndMoveTab设为truePageView
/// _moveSlider重复调用
_isClickAndMoveTab = true;
}
_isAddTabEvent = isAdd;
_currentSelectPage = page;
if (_controller.isAnimating) {
_controller.stop();
}
RenderBox slider =
_sliderKey.currentContext?.findRenderObject() as RenderBox;
Offset sliderPosition = slider.localToGlobal(Offset.zero);
RenderBox currentTabBox =
_tabKeys[page].currentContext?.findRenderObject() as RenderBox;
Offset currentTabPosition = currentTabBox.localToGlobal(Offset.zero);
_animTabWidth = currentTabBox.size.width;
final begin = sliderPosition.dx - _sliderFixMargin.value;
final end = currentTabPosition.dx +
(currentTabBox.size.width - _sliderWidth) / 2 -
_sliderFixMargin.value;
_sliderAnimation =
Tween<double>(begin: begin, end: end).animate(_curvedAnimation);
_controller.value = 0;
_controller.forward();
if (movePage) {
_pageController.animateToPage(page,
curve: Curves.linear, duration: Duration(milliseconds: 500));
}
} }
/// tab渲染完成才开始动画moveSlider /// tab渲染完成才开始动画moveSlider
Widget _animateTab({required Widget tab}) { Widget _animateTab({required Widget tab}) {
return Transform.translate( return Transform.translate(
offset: Offset( offset: Offset(
Tween<double>(begin: _isAddTabEvent ? -_animTabWidth : 0, end: 0) Tween<double>(
.evaluate(_curvedAnimation), begin: cascadeController.isAddTabEvent
? -cascadeController.animTabWidth
: 0,
end: 0)
.evaluate(cascadeController.curvedAnimation),
0), 0),
child: Opacity( child: Opacity(
/// ///
opacity: _isAnimateTextHide ? 0 : 1, opacity: cascadeController.isAnimateTextHide ? 0 : 1,
child: tab), child: tab),
); );
} }
List<Widget> _tabWidgets() { List<Widget> _tabWidgets() {
List<Widget> widgets = []; List<Widget> widgets = [];
_tabKeys.clear(); cascadeController.tabKeys.clear();
for (int i = 0; i < _pagesData.length; i++) { for (int i = 0; i < cascadeController.pagesData.length; i++) {
GlobalKey key = GlobalKey(); GlobalKey key = GlobalKey();
_tabKeys.add(key); cascadeController.tabKeys.add(key);
final tab = GestureDetector( final tab = GestureDetector(
child: Container( child: Container(
key: key, key: key,
height: widget.tabHeight, height: tabHeight,
color: widget.tabColor, color: tabColor,
alignment: Alignment.center, alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 15), child: Container(
child: ConstrainedBox( // constraints: BoxConstraints(
constraints: BoxConstraints( // maxWidth: MediaQuery.of(Get.context!).size.width /
maxWidth: // cascadeController.pagesData.length -
MediaQuery.of(context).size.width / _pagesData.length - 10), // 10),
child: Text( child: Text(
_selectedTabs[i], cascadeController.selectedTabs[i].label,
style: _currentSelectPage == i style: cascadeController.urrentSelectPage == i
? widget.tabTitleStyle.copyWith(color: widget.activeColor) ? tabTitleStyle.copyWith(color: activeColor)
: widget.tabTitleStyle, : tabTitleStyle,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
), ),
onTap: () { onTap: () {
_moveSlider(i); cascadeController.moveSlider(i);
}, },
); );
if (i == _pagesData.length - 1 && _selectedTabs[i] == _newTabName) { if (i == cascadeController.pagesData.length - 1 &&
cascadeController.selectedTabs[i].label ==
SkCascadePickerController.newTabName) {
widgets.add(_animateTab(tab: tab)); widgets.add(_animateTab(tab: tab));
_isAnimateTextHide = false;
} else { } else {
widgets.add(tab); widgets.add(tab);
} }
cascadeController.isAnimateTextHide = false;
if (i < cascadeController.pagesData.length - 1) {
widgets.add(Container(
// color: Colors.red,
child: Icon(
Icons.arrow_right,
size: ScreenAdaper.height(50),
),
));
}
} }
return widgets; return widgets;
} }
/// ///
Widget _pageItemWidget(int index, int page, String item) { Widget _pageItemWidget(int index, int page, CascadeItem item) {
return GestureDetector( return GestureDetector(
child: Container( child: Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 15), padding: EdgeInsets.symmetric(horizontal: ScreenAdaper.width(20)),
height: widget.itemHeight, height: itemHeight,
color: widget.itemColor, color: itemColor,
child: Row( child: Obx(() => Row(
children: [ children: [
item == _selectedTabs[page] item == cascadeController.selectedTabs[page]
? Padding( ? Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: widget.selectedIcon == null child: selectedIcon == null
? Icon(Icons.chevron_right, ? Icon(Icons.chevron_right,
size: 15, color: widget.activeColor) size: ScreenAdaper.height(40),
: widget.selectedIcon, color: activeColor)
) : selectedIcon,
: SizedBox(), )
Text("$item", : SizedBox(),
style: item == _selectedTabs[page] Text(item.label,
? widget.itemTitleStyle.copyWith(color: widget.activeColor) style: item == cascadeController.selectedTabs[page]
: widget.itemTitleStyle), ? itemTitleStyle.copyWith(color: activeColor)
], : itemTitleStyle),
), ],
)),
), ),
onTap: () { onTap: () {
if (page == widget.maxPageNum - 1) { if (page == maxPageNum - 1) {
/// ///
setState(() { cascadeController.selectedTabs[page] = item;
_selectedTabs[page] = item; cascadeController.selectedIndexes[page] = index;
_selectedIndexes[page] = index;
/// ///
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_moveSlider(page); cascadeController.moveSlider(page);
});
}); });
} else if (_tabKeys.length >= widget.maxPageNum || } else if (cascadeController.tabKeys.length >= maxPageNum ||
page < _tabKeys.length - 1) { page < cascadeController.tabKeys.length - 1) {
if (index == _selectedIndexes[page]) { if (index == cascadeController.selectedIndexes[page]) {
/// item /// item
_moveSlider(page + 1); cascadeController.moveSlider(page + 1);
} else { } else {
/// itemtab renderBox /// itemtab renderBox
setState(() { cascadeController.selectedTabs[page] = item;
_selectedTabs[page] = item; cascadeController.selectedIndexes[page] = index;
_selectedIndexes[page] = index; // selectedIndexes.removeRange(page + 1, selectedIndexes.length);
// _selectedIndexes.removeRange(page + 1, _selectedIndexes.length); cascadeController.loadNextPageData(page + 1, index, item,
}); isUpdatePage: true);
_loadNextPageData(page + 1, index, item, isUpdatePage: true);
} }
} else { } else {
/// tab页面 /// tab页面
/// page == _tabKeys.length - 1 && _tabKeys.length == widget.maxPageNum /// page == tabKeys.length - 1 && tabKeys.length == widget.maxPageNum
_selectedTabs[page] = item; cascadeController.selectedTabs[page] = item;
_selectedIndexes[page] = index; cascadeController.selectedIndexes[page] = index;
_addTab(page + 1, index, item); cascadeController.addTab(page + 1, index, item);
} }
}, },
); );
} }
Widget _pageWidget(int page) { Widget _pageWidget(int page) {
return ListView.builder( return Obx(() => ListView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemCount: _pagesData[page].length, itemCount: cascadeController.pagesData[page].length,
itemBuilder: (context, index) => itemBuilder: (context, index) => _pageItemWidget(
_pageItemWidget(index, page, _pagesData[page][index]), index, page, cascadeController.pagesData[page][index]),
// separatorBuilder: (context, index) => Divider(height: 0.3, thickness: 0.3, color: Color(0xffdddddd), indent: 15, endIndent: 15,), // separatorBuilder: (context, index) => Divider(height: 0.3, thickness: 0.3, color: Color(0xffdddddd), indent: 15, endIndent: 15,),
); ));
}
@override
void initState() {
super.initState();
_pagesData.add(widget.initialPageData);
_controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.ease)
..addStatusListener((state) {});
_sliderAnimation =
Tween<double>(begin: 0, end: 10).animate(_curvedAnimation);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
RenderBox tabBox =
_tabKeys.first.currentContext?.findRenderObject() as RenderBox;
_sliderFixMargin.value = (tabBox.size.width - _sliderWidth) / 2;
});
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedBuilder(
animation: _sliderAnimation!,
builder: (context, child) => Stack(
clipBehavior: Clip.hardEdge,
alignment: Alignment.bottomLeft,
children: [
Container(
width: MediaQuery.of(context).size.width,
child: Row(
children: _tabWidgets(),
),
),
ValueListenableBuilder<double>(
valueListenable: _sliderFixMargin,
builder: (_, margin, __) => Positioned(
left: margin + _sliderAnimation!.value,
child: Container(
key: _sliderKey,
width: _sliderWidth,
height: 2,
decoration: BoxDecoration(
color: widget.activeColor,
borderRadius: BorderRadius.circular(2)),
),
),
)
],
),
),
Expanded(
child: PageView.builder(
itemCount: _pagesData.length,
controller: _pageController,
itemBuilder: (context, index) => _pageWidget(index),
onPageChanged: (position) {
if (!_isClickAndMoveTab) {
_moveSlider(position, movePage: false);
}
if (_currentSelectPage == position) {
_isClickAndMoveTab = false;
}
},
),
)
],
);
} }
} }
class CascadeController extends GetxController { class SkCascadePickerController<T, Z> extends GetxController
late List<Item> items = []; with GetSingleTickerProviderStateMixin {
RxList<CascadeItem<T, Z>> treeData = RxList([]);
final Future<List<CascadeItem<T, Z>>> Function() getData;
final List<CascadeItem<T, Z>> Function(SkCascadePickerController<T, Z>)
initialPageData;
final NextPageCallback<T, Z> nextPageData;
final RxBool loading = false.obs;
final scrollController = ScrollController();
SkCascadePickerController(
{required this.initialPageData,
required this.nextPageData,
required this.getData});
@override @override
void onInit() { void onInit() {
items = []; init();
for (int i = 0; i < 5; i++) { animateController = AnimationController(
Item item0 = Item(); duration: const Duration(milliseconds: 500), vsync: this);
item0.name = "name_$i"; curvedAnimation =
item0.code = "code_$i"; CurvedAnimation(parent: animateController, curve: Curves.ease)
List<Item> children1 = []; ..addStatusListener((state) {});
for (int j = 0; j < 3; j++) { sliderAnimation = Tween<double>(begin: 0, end: 10).animate(curvedAnimation);
Item item1 = Item();
item1.name = "name_${i}_$j"; super.onInit();
item1.code = "code_${i}_$j"; }
List<Item> children2 = [];
for (int k = 0; k < 7; k++) { Future<void> init() async {
Item item2 = Item(); loading.value = true;
item2.name = "name_${i}_${j}_$k"; treeData.assignAll(await getData());
item2.code = "code_${i}_${j}_$k"; //
// 3 selectedTabs.add(CascadeItem<T, Z>(label: newTabName));
item2.children = []; pagesData.add(initialPageData(this));
children2.add(item2); WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
RenderBox tabBox =
tabKeys.first.currentContext?.findRenderObject() as RenderBox;
sliderFixMargin.value = (tabBox.size.width - sliderWidth) / 2;
});
loading.value = false;
}
bool isCompleted() =>
!selectedTabs.map((item) => item.label).contains(newTabName);
static String newTabName = "请选择";
late final AnimationController animateController;
late final CurvedAnimation curvedAnimation;
Animation? sliderAnimation;
final sliderFixMargin = ValueNotifier(0.0);
double sliderWidth = 20;
PageController pageController = PageController(initialPage: 0);
GlobalKey sliderKey = GlobalKey();
List<GlobalKey> tabKeys = [];
///
RxList<List<CascadeItem<T, Z>>> pagesData = RxList([]);
/// title集合
RxList<CascadeItem<T, Z>> selectedTabs = RxList([]);
/// item index集合
RxList<int> selectedIndexes = RxList([-1]);
/// "请选择"tab宽度tab时用到
double animTabWidth = 0;
/// tab添加事件记录"请选择"tab初始化状态
bool isAddTabEvent = false;
/// tab移动未开始'请选择'tab时隐藏文本tab在终点位置
bool isAnimateTextHide = false;
/// moveSlider重复调用
bool isClickAndMoveTab = false;
///
int urrentSelectPage = 0;
addTab(int page, int atIndex, CascadeItem<T, Z> currentPageItem) {
loadNextPageData(page, atIndex, currentPageItem);
}
loadNextPageData(int page, int atIndex, CascadeItem<T, Z> currentPageItem,
{bool isUpdatePage = false}) {
nextPageData((data) {
final nextPageDataIsEmpty = data.isEmpty;
if (!nextPageDataIsEmpty) {
///
if (isUpdatePage) {
///
pagesData[page] = data;
selectedTabs[page] = CascadeItem<T, Z>(label: newTabName);
selectedIndexes[page] = -1;
/// tab数据
pagesData.removeRange(page + 1, pagesData.length);
selectedIndexes.removeRange(page + 1, selectedIndexes.length);
selectedTabs.removeRange(page + 1, selectedTabs.length);
} else {
///
isAnimateTextHide = true;
isAddTabEvent = true;
pagesData.add(data);
selectedTabs.add(CascadeItem<T, Z>(label: newTabName));
selectedIndexes.add(-1);
} }
// 2 WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
item1.children = children2; moveSlider(page, isAdd: true);
children1.add(item1); });
} else {
///
final currentPage = page - 1;
selectedTabs[currentPage] = currentPageItem;
selectedIndexes[currentPage] = atIndex;
/// tab数据
pagesData.removeRange(page, pagesData.length);
selectedIndexes.removeRange(page, selectedIndexes.length);
selectedTabs.removeRange(page, selectedTabs.length);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//
moveSlider(currentPage);
});
} }
// 1 //
item0.children = children1; Future.delayed(const Duration(milliseconds: 500), () {
items.add(item0); scrollController.jumpTo(scrollController.position.maxScrollExtent);
super.onInit(); });
}, page, atIndex, currentPageItem, this);
}
moveSlider(int page, {bool movePage = true, bool isAdd = false}) {
if (movePage && urrentSelectPage != page) {
/// tab标签
/// isClickAndMoveTab设为truePageView
/// moveSlider重复调用
isClickAndMoveTab = true;
}
isAddTabEvent = isAdd;
urrentSelectPage = page;
if (animateController.isAnimating) {
animateController.stop();
}
RenderBox slider =
sliderKey.currentContext?.findRenderObject() as RenderBox;
Offset sliderPosition = slider.localToGlobal(Offset.zero);
RenderBox currentTabBox =
tabKeys[page].currentContext?.findRenderObject() as RenderBox;
Offset currentTabPosition = currentTabBox.localToGlobal(Offset.zero);
animTabWidth = currentTabBox.size.width;
final begin = sliderPosition.dx - sliderFixMargin.value;
final end = currentTabPosition.dx +
(currentTabBox.size.width - sliderWidth) / 2 -
sliderFixMargin.value;
sliderAnimation =
Tween<double>(begin: begin, end: end).animate(curvedAnimation);
animateController.value = 0;
animateController.forward();
if (movePage) {
pageController.animateToPage(page,
curve: Curves.linear, duration: Duration(milliseconds: 500));
} }
} }
late final _CascadePickerState _state; @override
void onClose() {
_setState(_CascadePickerState state) { animateController.dispose();
_state = state; pageController.dispose();
scrollController.dispose();
super.onClose();
} }
List<String> get selectedTitles => _state._selectedTabs;
List<int> get selectedIndexes => _state._selectedIndexes;
bool isCompleted() =>
!_state._selectedTabs.contains(_CascadePickerState._newTabName);
} }
/// pageData:
/// currentPage: ,
/// selectIndex:
typedef void NextPageCallback<T, Z>(
Function(List<CascadeItem<T, Z>>) pageData,
int currentPage,
int selectIndex,
CascadeItem<T, Z> currentPageItem,
SkCascadePickerController<T, Z> cascadeController);