QinGang_interested/lib/customWidget/department_all_person_picke...

789 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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(() {});
}
String _normalize(String? value) => (value ?? '').toLowerCase().trim();
bool _matchesQuery(Person p, String q) {
if (q.isEmpty) return true;
final name = _normalize(p.name);
final phone = _normalize(p.phone);
final departmentName = _normalize(p.departmentName);
final postName = _normalize(p.postName);
return name.contains(q) ||
phone.contains(q) ||
departmentName.contains(q) ||
postName.contains(q);
}
void _onSearchChanged() {
final q = _normalize(_searchController.text);
setState(() {
_personFiltered = _personAll.where((p) => _matchesQuery(p, q)).toList();
_serverFiltered = _serverAll.where((p) => _matchesQuery(p, 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),
],
),
),
);
}
}