部门多级选择器

main
hs 2025-07-24 11:18:47 +08:00
parent ccb9eb4f55
commit 824d5a403c
10 changed files with 212 additions and 193 deletions

View File

@ -6,6 +6,9 @@ PODS:
- Flutter (1.0.0)
- fluttertoast (0.0.2):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- mobile_scanner (7.0.0):
@ -26,6 +29,8 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
@ -38,6 +43,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- nfc_manager (from `.symlinks/plugins/nfc_manager/ios`)
@ -46,6 +52,7 @@ DEPENDENCIES:
- pdfx (from `.symlinks/plugins/pdfx/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
@ -58,6 +65,8 @@ EXTERNAL SOURCES:
:path: Flutter
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
mobile_scanner:
@ -74,6 +83,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/photo_manager/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
webview_flutter_wkwebview:
@ -84,6 +95,7 @@ SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
nfc_manager: f6d5609c09b4640b914a3dc67479a2e392965fd0
@ -92,6 +104,7 @@ SPEC CHECKSUMS:
pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2

View File

@ -1,99 +1,152 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/search_bar_widget.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import '../tools/tools.dart'; // SessionService
//
class Category {
final String id;
final String title;
final String name;
final List<Category> children;
Category({
required this.id,
required this.title,
required this.name,
this.children = const [],
});
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json['id'] as String,
name: json['name'] as String,
children: (json['children'] as List<dynamic>)
.map((e) => Category.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}
class DepartmentPicker extends StatefulWidget {
final List<Category> data;
final String? initialSelectedId;
final Set<String>? initialExpandedSet;
final ValueChanged<String?> onSelected;
/// id name
typedef DeptSelectCallback = void Function(String id, String name);
const DepartmentPicker({
Key? key,
required this.data,
this.initialSelectedId,
this.initialExpandedSet,
required this.onSelected,
}) : super(key: key);
class DepartmentPicker extends StatefulWidget {
/// id name
final DeptSelectCallback onSelected;
const DepartmentPicker({Key? key, required this.onSelected}) : super(key: key);
@override
_DepartmentPickerState createState() => _DepartmentPickerState();
}
class _DepartmentPickerState extends State<DepartmentPicker> {
late String? selectedId;
late Set<String> expandedSet;
String selectedId = '';
String selectedName = '';
Set<String> expandedSet = {};
List<Category> original = [];
List<Category> filtered = [];
bool loading = true;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
selectedId = widget.initialSelectedId;
expandedSet = Set<String>.from(widget.initialExpandedSet ?? {});
//
selectedId = '';
selectedName = '';
expandedSet = {};
_searchController.addListener(_onSearchChanged);
_loadData();
}
@override
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
super.dispose();
}
Future<void> _loadData() async {
try {
List<dynamic> raw;
if (SessionService.instance.departmentJsonStr?.isNotEmpty ?? false) {
raw = json.decode(SessionService.instance.departmentJsonStr!) as List<dynamic>;
} else {
final result = await ApiService.getHiddenTreatmentListTree();
final String nodes = result['zTreeNodes'] as String;
SessionService.instance.departmentJsonStr = nodes;
raw = json.decode(nodes) as List<dynamic>;
}
setState(() {
original = raw.map((e) => Category.fromJson(e as Map<String, dynamic>)).toList();
filtered = original;
loading = false;
});
} catch (e) {
setState(() => loading = false);
}
}
void _onSearchChanged() {
final query = _searchController.text.toLowerCase().trim();
setState(() {
filtered = query.isEmpty ? original : _filterCategories(original, query);
});
}
List<Category> _filterCategories(List<Category> list, String query) {
List<Category> result = [];
for (var cat in list) {
final children = _filterCategories(cat.children, query);
if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) {
result.add(Category(id: cat.id, name: cat.name, children: children));
}
}
return result;
}
Widget _buildRow(Category cat, int indent) {
final bool hasChildren = cat.children.isNotEmpty;
final bool isExpanded = expandedSet.contains(cat.id);
final bool isSelected = cat.id == selectedId;
final hasChildren = cat.children.isNotEmpty;
final isExpanded = expandedSet.contains(cat.id);
final isSelected = cat.id == selectedId;
return Column(
children: [
InkWell(
onTap: () {
setState(() {
if (hasChildren) {
if (isExpanded) {
expandedSet.remove(cat.id);
} else {
expandedSet.add(cat.id);
}
isExpanded ? expandedSet.remove(cat.id) : expandedSet.add(cat.id);
}
selectedId = cat.id;
selectedName = cat.name;
});
},
child: Container(
color: Colors.white,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
SizedBox(width: 16.0 * indent),
// /
SizedBox(
width: 24,
child: hasChildren
? Icon(
isExpanded ? Icons.expand_less : Icons.expand_more,
size: 20,
color: Colors.grey[600],
)
? Icon(isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_right_rounded,
size: 35, color: Colors.grey[600])
: const SizedBox.shrink(),
),
const SizedBox(width: 8),
//
const SizedBox(width: 5),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(cat.title),
child: Text(cat.name),
),
),
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
color: Colors.green,
),
),
@ -103,7 +156,6 @@ class _DepartmentPickerState extends State<DepartmentPicker> {
),
if (hasChildren && isExpanded)
...cat.children.map((c) => _buildRow(c, indent + 1)),
// const Divider(height: 1),
],
);
}
@ -113,35 +165,49 @@ class _DepartmentPickerState extends State<DepartmentPicker> {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.7,
color: Colors.transparent,
color: Colors.white,
child: Column(
children: [
//
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: const Text('取消', style: TextStyle(fontSize: 16)),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SearchBarWidget(
controller: _searchController,
isShowSearchButton: false,
onSearch: (keyboard) {
},
),
),
),
GestureDetector(
onTap: () => Navigator.of(context).pop(selectedId),
child: const Text('确定', style: TextStyle(fontSize: 16, color: Colors.green),),
onTap: () {
Navigator.of(context).pop();
widget.onSelected(selectedId, selectedName);
},
child: const Text('确定', style: TextStyle(fontSize: 16, color: Colors.green)),
),
],
),
),
const Divider(height: 1),
//
Divider(),
Expanded(
child: Container(
child: loading
? const Center(child: CircularProgressIndicator())
: Container(
color: Colors.white,
child: ListView.builder(
itemCount: widget.data.length,
itemBuilder: (ctx, idx) => _buildRow(widget.data[idx], 0),
itemCount: filtered.length,
itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0),
),
),
),

View File

@ -12,6 +12,7 @@ class SearchBarWidget extends StatelessWidget {
final bool isClickableOnly;
final VoidCallback? onInputTap;
final ValueChanged<String>? onTextChanged; //
final bool isShowSearchButton;
const SearchBarWidget({
Key? key,
@ -26,6 +27,7 @@ class SearchBarWidget extends StatelessWidget {
this.isClickableOnly = false,
this.onInputTap,
this.onTextChanged, //
this.isShowSearchButton = true,
}) : super(key: key);
//
@ -40,7 +42,6 @@ class SearchBarWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
@ -53,7 +54,8 @@ class SearchBarWidget extends StatelessWidget {
autofocus: autoFocus,
readOnly: isClickableOnly,
style: const TextStyle(fontSize: 15),
onChanged: onTextChanged, //
onChanged: onTextChanged,
//
decoration: InputDecoration(
filled: true,
fillColor: const Color(0xFFF5F5F5),
@ -65,7 +67,6 @@ class SearchBarWidget extends StatelessWidget {
),
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 8),
),
onSubmitted: onSearch,
),
@ -73,6 +74,7 @@ class SearchBarWidget extends StatelessWidget {
),
),
const SizedBox(width: 10),
if (isShowSearchButton)
//
ElevatedButton(
onPressed: () => onSearch(controller.text.trim()),
@ -87,10 +89,7 @@ class SearchBarWidget extends StatelessWidget {
),
child: Text(
buttonText,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
//
@ -112,10 +111,7 @@ class SearchBarWidget extends StatelessWidget {
),
child: Text(
resetButtonText,
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
],

View File

@ -925,7 +925,7 @@ U6Hzm1ninpWeE+awIDAQAB
);
}
///
///
static Future<Map<String, dynamic>> getHiddenTreatmentListTree() {
return HttpManager().request(
basePath,
@ -938,6 +938,21 @@ U6Hzm1ninpWeE+awIDAQAB
},
);
}
///
static Future<Map<String, dynamic>> getListTreePersonList(String DEPARTMENT_ID) {
return HttpManager().request(
basePath,
'app/sys/listUser',
method: Method.post,
data: {
"tm":DateTime.now().millisecondsSinceEpoch.toString(),
"DEPARTMENT_ID": DEPARTMENT_ID,
"CORPINFO_ID": SessionService.instance.corpinfoId,
"USER_ID": SessionService.instance.loginUserId,
},
);
}
///
static Future<Map<String, dynamic>> getDangerDetail(String id) {

View File

@ -25,28 +25,7 @@ class _CheckRecordListPageState extends State<CheckRecordListPage>
String time = '2025-06-${10 + i} 12:3${i}';
return NotificationItem(title, time);
});
final List<Category> data = [
Category(
id: '1',
title: '分类一',
children: [
Category(id: '1-1', title: '子项 1-1'),
Category(id: '1-2', title: '子项 1-2'),
],
),
Category(id: '2', title: '分类二'),
Category(
id: '3',
title: '分类三',
children: [
Category(
id: '3-1',
title: '子项 3-1',
children: [Category(id: '3-1-1', title: '子项 3-1-1')],
),
],
),
];
final TextEditingController _searchController = TextEditingController();
@override
@ -77,12 +56,9 @@ class _CheckRecordListPageState extends State<CheckRecordListPage>
barrierColor: Colors.black54,
backgroundColor: Colors.transparent,
builder:
(ctx) => DepartmentPicker(
data: data,
onSelected: (selectedId) {
setState(() {});
},
),
(ctx) => DepartmentPicker(onSelected: (id, name) {
}),
);
}

View File

@ -30,26 +30,6 @@ class _CustomRecordDrawerState extends State<CustomRecordDrawer> {
@override
Widget build(BuildContext context) {
final List<Category> data = [
Category(
id: '1',
title: '分类一1',
children: [
Category(id: '1-1', title: '子项 1-1'),
Category(id: '1-2', title: '子项 1-2'),
],
),
Category(id: '2', title: '分类二'),
Category(
id: '3',
title: '分类三',
children: [
Category(id: '3-1', title: '子项 3-1', children: [
Category(id: '3-1-1', title: '子项 3-1-1'),
]),
],
),
];
Future<void> showCategoryPicker(int type) async {
if (type == 1) {
@ -58,14 +38,11 @@ class _CustomRecordDrawerState extends State<CustomRecordDrawer> {
isScrollControlled: true,
barrierColor: Colors.black54,
backgroundColor: Colors.transparent,
builder: (ctx) => DepartmentPicker(
data: data,
onSelected: (selectedId) {
builder: (ctx) => DepartmentPicker(onSelected: (id, name) {
setState(() {
_selectedCategoryId = selectedId;
_selectedCategoryId = id;
});
},
),
}),
);
} else if (type == 2) {
final choice = await BottomPicker.show<String>(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
import 'package:qhd_prevention/customWidget/department_picker.dart';
import 'package:qhd_prevention/pages/home/tap/item_list_widget.dart';
import '../../../../../../http/ApiService.dart';
import '../../../../../my_appbar.dart';
enum EditUserType {
analyze,
@ -32,6 +34,8 @@ class HotworkApplyDetail extends StatefulWidget {
class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
final bool isEditable = true;
late String treeJson="";
Widget _defaultDetail() {
return Column(
@ -123,7 +127,16 @@ class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
}
///
void chooseUnitHandle(EditUserType type) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
barrierColor: Colors.black54,
backgroundColor: Colors.transparent,
builder: (ctx) => DepartmentPicker(onSelected: (id, name) {
setState(() {
});
}),
);
}
///
void choosePersonHandle(EditUserType type) {
@ -203,4 +216,11 @@ class _HotworkApplyDetailState extends State<HotworkApplyDetail> {
),
);
}
@override
void initState() {
// TODO: implement initState
super.initState();
}
}

View File

@ -32,28 +32,7 @@ class _AiAlarmPageState extends State<AiAlarmPage>
String time = '2025-06-${10 + i} 12:3${i}';
return NotificationItem(title, time);
});
final List<Category> data = [
Category(
id: '1',
title: '分类一',
children: [
Category(id: '1-1', title: '子项 1-1'),
Category(id: '1-2', title: '子项 1-2'),
],
),
Category(id: '2', title: '分类二'),
Category(
id: '3',
title: '分类三',
children: [
Category(
id: '3-1',
title: '子项 3-1',
children: [Category(id: '3-1-1', title: '子项 3-1-1')],
),
],
),
];
final TextEditingController _searchController = TextEditingController();
@override
@ -81,12 +60,9 @@ class _AiAlarmPageState extends State<AiAlarmPage>
barrierColor: Colors.black54,
backgroundColor: Colors.transparent,
builder:
(ctx) => DepartmentPicker(
data: data,
onSelected: (selectedId) {
setState(() {});
},
),
(ctx) => DepartmentPicker(onSelected: (id, name) {
}),
);
}

View File

@ -29,28 +29,7 @@ class _DangerPageState extends State<DangerPage>
String time = '2025-06-${10 + i} 12:3${i}';
return NotificationItem(title, time);
});
final List<Category> data = [
Category(
id: '1',
title: '分类一',
children: [
Category(id: '1-1', title: '子项 1-1'),
Category(id: '1-2', title: '子项 1-2'),
],
),
Category(id: '2', title: '分类二'),
Category(
id: '3',
title: '分类三',
children: [
Category(
id: '3-1',
title: '子项 3-1',
children: [Category(id: '3-1-1', title: '子项 3-1-1')],
),
],
),
];
final TextEditingController _searchController = TextEditingController();
@override
@ -83,12 +62,9 @@ class _DangerPageState extends State<DangerPage>
barrierColor: Colors.black54,
backgroundColor: Colors.transparent,
builder:
(ctx) => DepartmentPicker(
data: data,
onSelected: (selectedId) {
setState(() {});
},
),
(ctx) => DepartmentPicker(onSelected: (id, name) {
}),
);
}

View File

@ -157,6 +157,7 @@ class SessionService {
bool updateInfo = false;
String? dangerJson;
String? riskJson;
String? departmentJsonStr;
///
void loginSession(BuildContext context) {
@ -194,6 +195,9 @@ class SessionService {
void setRiskWaitInfo(String json) => riskJson = json;
void setDepartmentJsonStr(String json) => departmentJsonStr = json;
}