feat: cascade picker common widget developed
This commit is contained in:
parent
cee31e6896
commit
eabb746738
|
@ -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(
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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([]);
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ class EmployeeDetail extends StatelessWidget {
|
||||||
text: '考勤记录',
|
text: '考勤记录',
|
||||||
),
|
),
|
||||||
Tab(
|
Tab(
|
||||||
text: '工作安排',
|
text: '任务进度',
|
||||||
),
|
),
|
||||||
Tab(
|
Tab(
|
||||||
text: '相关文件',
|
text: '相关文件',
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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设为true,防止滑动PageView
|
|
||||||
/// 时_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 {
|
||||||
/// 选择不同的item,更新tab renderBox
|
/// 选择不同的item,更新tab 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设为true,防止滑动PageView
|
||||||
|
/// 时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);
|
||||||
|
|
Loading…
Reference in New Issue