QinGang_interested/lib/customWidget/department_all_person_picke...

786 lines
24 KiB
Dart
Raw Normal View History

2026-04-08 15:03:56 +08:00
// pubspec.yaml 需要添加依赖lpinyin: ^2.0.3
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:qhd_prevention/customWidget/search_bar_widget.dart';
/// 用户数据模型
class Person {
final String userId;
final String name;
final String departmentName;
final String phone;
final String postName;
final Map<String, dynamic>? raw;
Person({
required this.userId,
required this.name,
required this.departmentName,
required this.phone,
this.postName = '',
this.raw,
});
factory Person.fromJson(dynamic json) {
if (json is Map) {
return Person(
userId: (json['id'] ?? json['actUser'] ?? '').toString(),
name: (json['name'] ?? json['actUserName'] ??'').toString(),
departmentName: (json['departmentName'] ?? json['actUserDepartmentName'] ?? '').toString(),
phone: (json['phone'] ?? '').toString(),
postName: (json['postName'] ?? '').toString(),
raw: Map<String, dynamic>.from(json),
);
} else {
final s = json?.toString() ?? '';
return Person(
userId: s,
name: s,
departmentName: '',
phone: '',
postName: '',
raw: null,
);
}
}
}
/// 回调 typedef保留原有
typedef PersonSelectCallback = void Function(String userId, String name);
typedef PersonSelectCallbackWithIndex = void Function(String userId, String name, int index);
typedef PersonSelectCallbackWithData = void Function(String userId, String name, Map<String, dynamic> data);
typedef PersonSelectCallbackWithIndexAndData = void Function(
String userId,
String name,
int index,
Map<String, dynamic> data,
);
typedef PersonMultiSelectCallback = void Function(List<Map<String, dynamic>> selectedRawList);
/// DepartmentAllPersonPicker
class DepartmentAllPersonPicker {
/// show 参数说明
/// - personsData第一个 Tab分公司
/// - serverData第二个 Tab相关方
/// - allowXgfFlagfalse 时不显示 Tab仅显示 personsData
static Future<void> show(
BuildContext context, {
required List<dynamic> personsData,
List<dynamic>? serverData,
bool allowXgfFlag = true,
bool multiSelect = false,
PersonMultiSelectCallback? onMultiSelected,
PersonSelectCallback? onSelected,
PersonSelectCallbackWithIndex? onSelectedWithIndex,
PersonSelectCallbackWithData? onSelectedWithData,
PersonSelectCallbackWithIndexAndData? onSelectedWithIndexWithData,
}) async {
assert(
(!multiSelect &&
(onSelected != null ||
onSelectedWithIndex != null ||
onSelectedWithData != null ||
onSelectedWithIndexWithData != null)) ||
(multiSelect &&
(onMultiSelected != null ||
onSelectedWithData != null ||
onSelectedWithIndexWithData != null ||
onSelectedWithIndex != null ||
onSelected != null)),
'请至少传入一个回调(单选或多选)',
);
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _DepartmentAllPersonPickerSheet(
personsData: personsData,
serverData: serverData,
allowXgfFlag: allowXgfFlag,
multiSelect: multiSelect,
onMultiSelected: onMultiSelected,
onSelected: onSelected,
onSelectedWithIndex: onSelectedWithIndex,
onSelectedWithData: onSelectedWithData,
onSelectedWithIndexWithData: onSelectedWithIndexWithData,
),
);
}
}
class _DepartmentAllPersonPickerSheet extends StatefulWidget {
final List<dynamic> personsData;
final List<dynamic>? serverData;
final bool allowXgfFlag;
final bool multiSelect;
final PersonMultiSelectCallback? onMultiSelected;
final PersonSelectCallback? onSelected;
final PersonSelectCallbackWithIndex? onSelectedWithIndex;
final PersonSelectCallbackWithData? onSelectedWithData;
final PersonSelectCallbackWithIndexAndData? onSelectedWithIndexWithData;
const _DepartmentAllPersonPickerSheet({
required this.personsData,
required this.serverData,
required this.allowXgfFlag,
required this.multiSelect,
required this.onMultiSelected,
required this.onSelected,
required this.onSelectedWithIndex,
required this.onSelectedWithData,
required this.onSelectedWithIndexWithData,
});
@override
State<_DepartmentAllPersonPickerSheet> createState() => _DepartmentAllPersonPickerSheetState();
}
class _DepartmentAllPersonPickerSheetState extends State<_DepartmentAllPersonPickerSheet>
with SingleTickerProviderStateMixin {
late final List<Person> _personAll;
late final List<Person> _serverAll;
List<Person> _personFiltered = [];
List<Person> _serverFiltered = [];
final TextEditingController _searchController = TextEditingController();
final ScrollController _personController = ScrollController();
final ScrollController _serverController = ScrollController();
TabController? _tabController;
final Map<int, String> _currentLetters = {0: '', 1: ''};
String _singleSelectedKey = '';
String _singleSelectedId = '';
String _singleSelectedName = '';
Person? _singleSelectedPerson;
int _singleSelectedSourceIndex = -1;
String _singleSelectedSource = '';
final Set<String> _multiSelectedKeys = <String>{};
static const double _headerHeight = 40.0;
static const double _itemHeight = 50.0;
static const double _rightReservedWidth = 30.0;
static const double _trailingWidth = 56.0;
bool get _showTabs => widget.allowXgfFlag;
@override
void initState() {
super.initState();
// personsData去重
final Map<dynamic, dynamic> uniqueMap = {};
for (final item in widget.personsData) {
if (item is Map<String, dynamic>) {
final key = item['id'] ?? item['actUser']; // id去重
uniqueMap[key] = item;
}
}
final List<dynamic> list = uniqueMap.values.toList();
_personAll = list.map((e) => Person.fromJson(e)).toList();
_serverAll = (widget.serverData ?? []).map((e) => Person.fromJson(e)).toList();
_personFiltered = List.from(_personAll);
_serverFiltered = List.from(_serverAll);
if (_showTabs) {
_tabController = TabController(length: 2, vsync: this);
_tabController!.addListener(_onTabChanged);
}
_searchController.addListener(_onSearchChanged);
_personController.addListener(() => _updateCurrentLetter(0));
_serverController.addListener(() => _updateCurrentLetter(1));
}
@override
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
_personController.dispose();
_serverController.dispose();
_tabController?.removeListener(_onTabChanged);
_tabController?.dispose();
super.dispose();
}
void _onTabChanged() {
setState(() {});
}
void _onSearchChanged() {
final q = _searchController.text.toLowerCase().trim();
setState(() {
_personFiltered = q.isEmpty
? List.from(_personAll)
: _personAll.where((p) {
final nameLower = p.name.toLowerCase();
final phoneLower = p.phone.toLowerCase();
return nameLower.contains(q) || phoneLower.contains(q);
}).toList();
_serverFiltered = q.isEmpty
? List.from(_serverAll)
: _serverAll.where((p) {
final nameLower = p.name.toLowerCase();
final phoneLower = p.phone.toLowerCase();
return nameLower.contains(q) || phoneLower.contains(q);
}).toList();
});
_updateCurrentLetter(0);
_updateCurrentLetter(1);
}
String _itemKey(String source, String id) => '$source::$id';
String _getInitial(String name) {
final trimmed = name.trim();
if (trimmed.isEmpty) return '#';
final first = trimmed[0];
if (RegExp(r'[A-Za-z]').hasMatch(first)) return first.toUpperCase();
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(first)) {
try {
final short = PinyinHelper.getShortPinyin(trimmed);
if (short.isNotEmpty) return short[0].toUpperCase();
} catch (_) {}
}
return '#';
}
bool _detectIsRelated(dynamic raw) {
try {
if (raw is Map) {
if (raw.containsKey('isXgf') || raw.containsKey('isxgf')) {
final v = raw['isXgf'] ?? raw['isxgf'];
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) return v == '1' || v.toLowerCase() == 'true';
}
if (raw.containsKey('isRelated') || raw.containsKey('isrelated') || raw.containsKey('related')) {
final v = raw['isRelated'] ?? raw['isrelated'] ?? raw['related'];
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) return v == '1' || v.toLowerCase() == 'true';
}
final dept = (raw['departmentName'] ?? raw['deptName'] ?? raw['department'] ?? '').toString();
if (dept.contains('相关')) return true;
if (raw.containsKey('allowXgfFlag')) {
final v = raw['allowXgfFlag'];
if (v is bool) return !v;
if (v is num) return v == 0;
if (v is String) return v == '0' || v.toLowerCase() == 'false';
}
}
} catch (_) {}
return false;
}
bool _canSelectByAllowFlag(dynamic raw) {
if (widget.allowXgfFlag) return true;
return !_detectIsRelated(raw);
}
Map<String, List<Person>> _buildGroupMap(List<Person> source) {
final Map<String, List<Person>> map = {};
for (final p in source) {
final key = _getInitial(p.name);
map.putIfAbsent(key, () => []).add(p);
}
for (final k in map.keys) {
map[k]!.sort((a, b) => a.name.compareTo(b.name));
}
return map;
}
List<String> _orderedKeys(Map<String, List<Person>> groupMap) {
final alphaKeys = groupMap.keys
.where((k) => RegExp(r'^[A-Z]$').hasMatch(k))
.toList()
..sort();
final otherKeys = groupMap.keys
.where((k) => !RegExp(r'^[A-Z]$').hasMatch(k))
.toList()
..sort();
return [...alphaKeys, ...otherKeys];
}
Map<String, double> _computeOffsets(List<String> keys, Map<String, List<Person>> map) {
final offsets = <String, double>{};
double cursor = 0.0;
for (final k in keys) {
offsets[k] = cursor;
cursor += _headerHeight + map[k]!.length * _itemHeight;
}
return offsets;
}
void _updateCurrentLetter(int tabIndex) {
final source = tabIndex == 0 ? _personFiltered : _serverFiltered;
final controller = tabIndex == 0 ? _personController : _serverController;
if (!controller.hasClients) return;
if (source.isEmpty) {
if (_currentLetters[tabIndex] != '') {
setState(() => _currentLetters[tabIndex] = '');
}
return;
}
final groupMap = _buildGroupMap(source);
final keys = _orderedKeys(groupMap);
if (keys.isEmpty) return;
final offsets = _computeOffsets(keys, groupMap);
final pos = controller.position.pixels;
String found = keys.first;
for (final k in keys) {
final off = offsets[k] ?? double.infinity;
if (pos >= off) {
found = k;
} else {
break;
}
}
if (_currentLetters[tabIndex] != found) {
setState(() {
_currentLetters[tabIndex] = found;
});
}
}
Future<void> _scrollToLetter(
String letter, {
required int tabIndex,
required List<String> keys,
required Map<String, double> offsets,
}) async {
final controller = tabIndex == 0 ? _personController : _serverController;
final targetOffset = offsets[letter] ?? 0.0;
if (!controller.hasClients) return;
try {
await controller.animateTo(
targetOffset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
} catch (_) {}
setState(() {
_currentLetters[tabIndex] = letter;
});
}
String _buildTitleText(Person person) {
if (person.postName.isEmpty && person.phone.isEmpty && person.departmentName.isEmpty) return '';
final postPart = person.postName.isNotEmpty ? '/${person.postName}' : '';
return '${person.name}-${person.phone}${person.departmentName}$postPart';
}
void _onItemTap({
required Person item,
required int sourceIndex,
required String source,
}) {
final raw = item.raw;
if (!_canSelectByAllowFlag(raw)) return;
final key = _itemKey(source, item.userId);
setState(() {
if (widget.multiSelect) {
if (_multiSelectedKeys.contains(key)) {
_multiSelectedKeys.remove(key);
} else {
_multiSelectedKeys.add(key);
}
} else {
_singleSelectedKey = key;
_singleSelectedId = item.userId;
_singleSelectedName = item.name;
_singleSelectedPerson = item;
_singleSelectedSourceIndex = sourceIndex;
_singleSelectedSource = source;
}
});
}
bool _confirmEnabled() {
if (widget.multiSelect) {
return _multiSelectedKeys.isNotEmpty;
}
return _singleSelectedKey.isNotEmpty;
}
Map<String, dynamic> _toDataMap(Person person) {
return person.raw ??
<String, dynamic>{
'id': person.userId,
'name': person.name,
'departmentName': person.departmentName,
'phone': person.phone,
'postName': person.postName,
};
}
Person? _findPersonByKey(String key) {
final parts = key.split('::');
if (parts.length != 2) return null;
final source = parts[0];
final id = parts[1];
if (source == 'person') {
for (final p in _personAll) {
if (p.userId == id) return p;
}
} else if (source == 'server') {
for (final p in _serverAll) {
if (p.userId == id) return p;
}
}
return null;
}
Widget _buildGroupedList({
required List<Person> source,
required ScrollController controller,
required int tabIndex,
required String emptyText,
}) {
if (source.isEmpty) {
return Center(
child: Text(
emptyText,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
);
}
final groupMap = _buildGroupMap(source);
final keys = _orderedKeys(groupMap);
final offsets = _computeOffsets(keys, groupMap);
final currentLetter = _currentLetters[tabIndex] ?? '';
return Stack(
children: [
ListView(
controller: controller,
padding: EdgeInsets.zero,
children: keys.expand((k) {
final items = groupMap[k]!;
return <Widget>[
Container(
height: _headerHeight,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.grey[200],
alignment: Alignment.centerLeft,
child: Text(
k,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
...items.asMap().entries.map((entry) {
final idx = entry.key;
final item = entry.value;
final sourceIndex = source.indexWhere((p) => p.userId == item.userId);
final raw = item.raw;
final isRelated = !_canSelectByAllowFlag(raw);
final selectedKey = _itemKey(tabIndex == 0 ? 'person' : 'server', item.userId);
final selected = widget.multiSelect
? _multiSelectedKeys.contains(selectedKey)
: (_singleSelectedKey == selectedKey);
return InkWell(
onTap: isRelated
? null
: () => _onItemTap(
item: item,
sourceIndex: sourceIndex >= 0 ? sourceIndex : idx,
source: tabIndex == 0 ? 'person' : 'server',
),
child: Container(
height: _itemHeight,
padding: const EdgeInsets.only(left: 16, right: _rightReservedWidth),
alignment: Alignment.centerLeft,
color: isRelated ? Colors.grey[50] : Colors.white,
child: Row(
children: [
Expanded(
child: Text(
_buildTitleText(item),
style: TextStyle(
color: isRelated ? Colors.grey : Colors.black,
),
),
),
SizedBox(
width: _trailingWidth,
child: selected
? const Align(
alignment: Alignment.centerRight,
child: Icon(Icons.check, color: Colors.blue),
)
: const SizedBox.shrink(),
),
],
),
),
);
}),
];
}).toList(),
),
if (keys.isNotEmpty)
Positioned(
right: 4,
top: 100,
bottom: 100,
child: Column(
mainAxisSize: MainAxisSize.min,
children: keys.map((letter) {
final isActive = letter == currentLetter;
return GestureDetector(
onTap: () => _scrollToLetter(
letter,
tabIndex: tabIndex,
keys: keys,
offsets: offsets,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text(
letter,
style: TextStyle(
fontSize: 12,
color: isActive ? Colors.blue : Colors.black54,
),
),
),
);
}).toList(),
),
),
],
);
}
void _confirm() {
if (!mounted) return;
if (widget.multiSelect) {
final selectedRaw = _multiSelectedKeys.map((key) {
final person = _findPersonByKey(key);
if (person != null) {
return _toDataMap(person);
}
final parts = key.split('::');
final id = parts.length == 2 ? parts[1] : key;
return <String, dynamic>{
'id': id,
'username': '',
};
}).toList();
if (widget.onMultiSelected != null) {
widget.onMultiSelected!(selectedRaw);
return;
}
if (_multiSelectedKeys.isEmpty) return;
final firstKey = _multiSelectedKeys.first;
final firstPerson = _findPersonByKey(firstKey);
final firstId = firstPerson?.userId ?? '';
final firstName = firstPerson?.name ?? '';
if (firstPerson != null) {
final idx = firstKey.startsWith('person::')
? _personAll.indexWhere((p) => p.userId == firstId)
: _serverAll.indexWhere((p) => p.userId == firstId);
final dataMap = _toDataMap(firstPerson);
if (widget.onSelectedWithIndexWithData != null) {
widget.onSelectedWithIndexWithData!(firstId, firstName, idx, dataMap);
return;
}
if (widget.onSelectedWithData != null) {
widget.onSelectedWithData!(firstId, firstName, dataMap);
return;
}
if (widget.onSelectedWithIndex != null) {
widget.onSelectedWithIndex!(firstId, firstName, idx);
return;
}
if (widget.onSelected != null) {
widget.onSelected!(firstId, firstName);
return;
}
}
return;
}
final selectedPerson = _singleSelectedPerson;
if (selectedPerson == null) return;
final dataMap = _toDataMap(selectedPerson);
if (widget.onSelectedWithIndexWithData != null) {
widget.onSelectedWithIndexWithData!(
selectedPerson.userId,
selectedPerson.name,
_singleSelectedSourceIndex,
dataMap,
);
return;
}
if (widget.onSelectedWithData != null) {
widget.onSelectedWithData!(
selectedPerson.userId,
selectedPerson.name,
dataMap,
);
return;
}
if (widget.onSelectedWithIndex != null) {
widget.onSelectedWithIndex!(
selectedPerson.userId,
selectedPerson.name,
_singleSelectedSourceIndex,
);
return;
}
if (widget.onSelected != null) {
widget.onSelected!(selectedPerson.userId, selectedPerson.name);
return;
}
}
@override
Widget build(BuildContext context) {
final showTabs = _showTabs;
final tabIndex = _tabController?.index ?? 0;
final personGroupMap = _buildGroupMap(_personFiltered);
final serverGroupMap = _buildGroupMap(_serverFiltered);
final personKeys = _orderedKeys(personGroupMap);
final serverKeys = _orderedKeys(serverGroupMap);
final personOffsets = _computeOffsets(personKeys, personGroupMap);
final serverOffsets = _computeOffsets(serverKeys, serverGroupMap);
final personListWidget = _buildGroupedList(
source: _personFiltered,
controller: _personController,
tabIndex: 0,
emptyText: '暂无数据',
);
final serverListWidget = _buildGroupedList(
source: _serverFiltered,
controller: _serverController,
tabIndex: 1,
emptyText: '暂无数据',
);
Widget contentWidget;
if (!showTabs) {
contentWidget = personListWidget;
} else {
contentWidget = TabBarView(
controller: _tabController,
children: [
personListWidget,
serverListWidget,
],
);
}
return SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.82,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消', style: TextStyle(fontSize: 16)),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SearchBarWidget(
controller: _searchController,
isShowSearchButton: false,
onSearch: (keyboard) {
_onSearchChanged();
},
),
),
),
TextButton(
onPressed: _confirmEnabled()
? () {
Navigator.of(context).pop();
_confirm();
}
: null,
child: Text(
'确定',
style: TextStyle(
color: _confirmEnabled() ? Colors.blue : Colors.grey,
fontSize: 16,
),
),
),
],
),
),
const Divider(height: 1),
if (showTabs) ...[
TabBar(
controller: _tabController,
tabs: const [
Tab(text: '分公司'),
Tab(text: '相关方'),
],
indicatorColor: Colors.blue,
labelColor: Colors.blue,
unselectedLabelColor: Colors.black54,
),
const Divider(height: 1),
],
Expanded(child: contentWidget),
],
),
),
);
}
}