| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  | import 'package:flutter/cupertino.dart'; | 
					
						
							|  |  |  |  | import 'package:flutter/material.dart'; | 
					
						
							|  |  |  |  | import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; | 
					
						
							|  |  |  |  | /// 用户数据模型
 | 
					
						
							|  |  |  |  | class Person { | 
					
						
							|  |  |  |  |   final String userId; | 
					
						
							|  |  |  |  |   final String name; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   Person({required this.userId, required this.name}); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   factory Person.fromJson(Map<String, dynamic> json) { | 
					
						
							|  |  |  |  |     return Person( | 
					
						
							|  |  |  |  |       userId: json['USER_ID'] as String, | 
					
						
							|  |  |  |  |       name: json['NAME'] as String, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  | /// 原回调签名(向后兼容)
 | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  | typedef PersonSelectCallback = void Function(String userId, String name); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  | /// 新回调签名,增加可选 index(int,默认 0)
 | 
					
						
							|  |  |  |  | typedef PersonSelectCallbackWithIndex = void Function(String userId, String name, int index); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  | /// 底部弹窗人员选择器(使用预先传入的原始数据列表,不做接口请求)
 | 
					
						
							|  |  |  |  | class DepartmentPersonPicker { | 
					
						
							|  |  |  |  |   /// 显示人员选择弹窗
 | 
					
						
							|  |  |  |  |   ///
 | 
					
						
							|  |  |  |  |   /// [personsData]: 已拉取并缓存的原始 Map 列表
 | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |   /// [onSelected]: 选中后回调 USER_ID 和 NAME(向后兼容旧代码)
 | 
					
						
							|  |  |  |  |   /// [onSelectedWithIndex]: 可选的新回调,额外返回 index(index 为在原始 personsData/_all 中的下标,找不到则为 0)
 | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |   static Future<void> show( | 
					
						
							|  |  |  |  |       BuildContext context, { | 
					
						
							|  |  |  |  |         required List<Map<String, dynamic>> personsData, | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |         PersonSelectCallback? onSelected, | 
					
						
							|  |  |  |  |         PersonSelectCallbackWithIndex? onSelectedWithIndex, | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |       }) async { | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |     // 至少传入一个回调(保持对旧调用的兼容)
 | 
					
						
							|  |  |  |  |     assert(onSelected != null || onSelectedWithIndex != null, | 
					
						
							|  |  |  |  |     '请至少传入 onSelected 或 onSelectedWithIndex'); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |     // 转换为模型
 | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |     final List<Person> _all = personsData.map((e) => Person.fromJson(e)).toList(); | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |     List<Person> _filtered = List.from(_all); | 
					
						
							|  |  |  |  |     String _selectedName = ''; | 
					
						
							|  |  |  |  |     String _selectedId = ''; | 
					
						
							|  |  |  |  |     final TextEditingController _searchController = TextEditingController(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     await showModalBottomSheet( | 
					
						
							|  |  |  |  |       context: context, | 
					
						
							|  |  |  |  |       isScrollControlled: true, | 
					
						
							|  |  |  |  |       backgroundColor: Colors.white, | 
					
						
							|  |  |  |  |       shape: const RoundedRectangleBorder( | 
					
						
							|  |  |  |  |         borderRadius: BorderRadius.vertical(top: Radius.circular(12)), | 
					
						
							|  |  |  |  |       ), | 
					
						
							|  |  |  |  |       builder: (ctx) { | 
					
						
							|  |  |  |  |         return StatefulBuilder( | 
					
						
							|  |  |  |  |           builder: (BuildContext ctx, StateSetter setState) { | 
					
						
							|  |  |  |  |             // 搜索逻辑
 | 
					
						
							|  |  |  |  |             void _onSearch(String v) { | 
					
						
							|  |  |  |  |               final q = v.toLowerCase().trim(); | 
					
						
							|  |  |  |  |               setState(() { | 
					
						
							|  |  |  |  |                 _filtered = q.isEmpty | 
					
						
							|  |  |  |  |                     ? List.from(_all) | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                     : _all.where((p) => p.name.toLowerCase().contains(q)).toList(); | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |               }); | 
					
						
							|  |  |  |  |             } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |             return SafeArea( | 
					
						
							|  |  |  |  |               child: SizedBox( | 
					
						
							|  |  |  |  |                 height: MediaQuery.of(ctx).size.height * 0.75, | 
					
						
							|  |  |  |  |                 child: Column( | 
					
						
							|  |  |  |  |                   children: [ | 
					
						
							|  |  |  |  |                     // 顶部:取消、搜索、确定
 | 
					
						
							|  |  |  |  |                     Padding( | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                       child: Row( | 
					
						
							|  |  |  |  |                         children: [ | 
					
						
							|  |  |  |  |                           TextButton( | 
					
						
							|  |  |  |  |                             onPressed: () => Navigator.of(ctx).pop(), | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                             child: const Text( | 
					
						
							|  |  |  |  |                               '取消', | 
					
						
							|  |  |  |  |                               style: TextStyle(fontSize: 16), | 
					
						
							|  |  |  |  |                             ), | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                           ), | 
					
						
							|  |  |  |  |                           Expanded( | 
					
						
							|  |  |  |  |                             child: Padding( | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                               padding: const EdgeInsets.symmetric(horizontal: 8), | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                               child: SearchBarWidget( | 
					
						
							|  |  |  |  |                                 controller: _searchController, | 
					
						
							|  |  |  |  |                                 onTextChanged: _onSearch, | 
					
						
							|  |  |  |  |                                 isShowSearchButton: false, | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                                 onSearch: (keyboard) {}, | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                               ), | 
					
						
							|  |  |  |  |                             ), | 
					
						
							|  |  |  |  |                           ), | 
					
						
							|  |  |  |  |                           TextButton( | 
					
						
							|  |  |  |  |                             onPressed: _selectedId.isEmpty | 
					
						
							|  |  |  |  |                                 ? null | 
					
						
							|  |  |  |  |                                 : () { | 
					
						
							|  |  |  |  |                               Navigator.of(ctx).pop(); | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |                               // 计算 index(在原始 _all 列表中的下标)
 | 
					
						
							|  |  |  |  |                               final idx = _all.indexWhere((p) => p.userId == _selectedId); | 
					
						
							|  |  |  |  |                               final validIndex = idx >= 0 ? idx : 0; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |                               // 优先调用带 index 的回调(新),否则调用旧回调
 | 
					
						
							|  |  |  |  |                               if (onSelectedWithIndex != null) { | 
					
						
							|  |  |  |  |                                 onSelectedWithIndex(_selectedId, _selectedName, validIndex); | 
					
						
							|  |  |  |  |                               } else if (onSelected != null) { | 
					
						
							|  |  |  |  |                                 onSelected(_selectedId, _selectedName); | 
					
						
							|  |  |  |  |                               } | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                             }, | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                             child: const Text( | 
					
						
							|  |  |  |  |                               '确定', | 
					
						
							|  |  |  |  |                               style: TextStyle(color: Colors.green, fontSize: 16), | 
					
						
							|  |  |  |  |                             ), | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                           ), | 
					
						
							|  |  |  |  |                         ], | 
					
						
							|  |  |  |  |                       ), | 
					
						
							|  |  |  |  |                     ), | 
					
						
							|  |  |  |  |                     const Divider(height: 1), | 
					
						
							|  |  |  |  |                     // 列表
 | 
					
						
							|  |  |  |  |                     Expanded( | 
					
						
							|  |  |  |  |                       child: ListView.separated( | 
					
						
							|  |  |  |  |                         itemCount: _filtered.length, | 
					
						
							|  |  |  |  |                         separatorBuilder: (_, __) => const Divider(height: 1), | 
					
						
							|  |  |  |  |                         itemBuilder: (context, index) { | 
					
						
							|  |  |  |  |                           final person = _filtered[index]; | 
					
						
							|  |  |  |  |                           final selected = person.userId == _selectedId; | 
					
						
							|  |  |  |  |                           return ListTile( | 
					
						
							|  |  |  |  |                             titleAlignment: ListTileTitleAlignment.center, | 
					
						
							|  |  |  |  |                             title: Text(person.name), | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  |                             trailing: selected ? const Icon(Icons.check, color: Colors.green) : null, | 
					
						
							| 
									
										
										
										
											2025-07-24 14:49:16 +08:00
										 |  |  |  |                             onTap: () => setState(() { | 
					
						
							|  |  |  |  |                               _selectedId = person.userId; | 
					
						
							|  |  |  |  |                               _selectedName = person.name; | 
					
						
							|  |  |  |  |                             }), | 
					
						
							|  |  |  |  |                           ); | 
					
						
							|  |  |  |  |                         }, | 
					
						
							|  |  |  |  |                       ), | 
					
						
							|  |  |  |  |                     ), | 
					
						
							|  |  |  |  |                   ], | 
					
						
							|  |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ), | 
					
						
							|  |  |  |  |             ); | 
					
						
							|  |  |  |  |           }, | 
					
						
							|  |  |  |  |         ); | 
					
						
							|  |  |  |  |       }, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-16 14:13:23 +08:00
										 |  |  |  | } |