152 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			152 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Dart
		
	
	
| 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,
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// 原回调签名(向后兼容)
 | ||
| typedef PersonSelectCallback = void Function(String userId, String name);
 | ||
| 
 | ||
| /// 新回调签名,增加可选 index(int,默认 0)
 | ||
| typedef PersonSelectCallbackWithIndex = void Function(String userId, String name, int index);
 | ||
| 
 | ||
| /// 底部弹窗人员选择器(使用预先传入的原始数据列表,不做接口请求)
 | ||
| class DepartmentPersonPicker {
 | ||
|   /// 显示人员选择弹窗
 | ||
|   ///
 | ||
|   /// [personsData]: 已拉取并缓存的原始 Map 列表
 | ||
|   /// [onSelected]: 选中后回调 USER_ID 和 NAME(向后兼容旧代码)
 | ||
|   /// [onSelectedWithIndex]: 可选的新回调,额外返回 index(index 为在原始 personsData/_all 中的下标,找不到则为 0)
 | ||
|   static Future<void> show(
 | ||
|       BuildContext context, {
 | ||
|         required List<Map<String, dynamic>> personsData,
 | ||
|         PersonSelectCallback? onSelected,
 | ||
|         PersonSelectCallbackWithIndex? onSelectedWithIndex,
 | ||
|       }) async {
 | ||
|     // 至少传入一个回调(保持对旧调用的兼容)
 | ||
|     assert(onSelected != null || onSelectedWithIndex != null,
 | ||
|     '请至少传入 onSelected 或 onSelectedWithIndex');
 | ||
| 
 | ||
|     // 转换为模型
 | ||
|     final List<Person> _all = personsData.map((e) => Person.fromJson(e)).toList();
 | ||
|     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)
 | ||
|                     : _all.where((p) => p.name.toLowerCase().contains(q)).toList();
 | ||
|               });
 | ||
|             }
 | ||
| 
 | ||
|             return SafeArea(
 | ||
|               child: SizedBox(
 | ||
|                 height: MediaQuery.of(ctx).size.height * 0.75,
 | ||
|                 child: Column(
 | ||
|                   children: [
 | ||
|                     // 顶部:取消、搜索、确定
 | ||
|                     Padding(
 | ||
|                       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | ||
|                       child: Row(
 | ||
|                         children: [
 | ||
|                           TextButton(
 | ||
|                             onPressed: () => Navigator.of(ctx).pop(),
 | ||
|                             child: const Text(
 | ||
|                               '取消',
 | ||
|                               style: TextStyle(fontSize: 16),
 | ||
|                             ),
 | ||
|                           ),
 | ||
|                           Expanded(
 | ||
|                             child: Padding(
 | ||
|                               padding: const EdgeInsets.symmetric(horizontal: 8),
 | ||
|                               child: SearchBarWidget(
 | ||
|                                 controller: _searchController,
 | ||
|                                 onTextChanged: _onSearch,
 | ||
|                                 isShowSearchButton: false,
 | ||
|                                 onSearch: (keyboard) {},
 | ||
|                               ),
 | ||
|                             ),
 | ||
|                           ),
 | ||
|                           TextButton(
 | ||
|                             onPressed: _selectedId.isEmpty
 | ||
|                                 ? null
 | ||
|                                 : () {
 | ||
|                               Navigator.of(ctx).pop();
 | ||
| 
 | ||
|                               // 计算 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);
 | ||
|                               }
 | ||
|                             },
 | ||
|                             child: const Text(
 | ||
|                               '确定',
 | ||
|                               style: TextStyle(color: Colors.green, fontSize: 16),
 | ||
|                             ),
 | ||
|                           ),
 | ||
|                         ],
 | ||
|                       ),
 | ||
|                     ),
 | ||
|                     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),
 | ||
|                             trailing: selected ? const Icon(Icons.check, color: Colors.green) : null,
 | ||
|                             onTap: () => setState(() {
 | ||
|                               _selectedId = person.userId;
 | ||
|                               _selectedName = person.name;
 | ||
|                             }),
 | ||
|                           );
 | ||
|                         },
 | ||
|                       ),
 | ||
|                     ),
 | ||
|                   ],
 | ||
|                 ),
 | ||
|               ),
 | ||
|             );
 | ||
|           },
 | ||
|         );
 | ||
|       },
 | ||
|     );
 | ||
|   }
 | ||
| } |