232 lines
9.8 KiB
Dart
232 lines
9.8 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;
|
|||
|
|
final String departmentName;
|
|||
|
|
final String phone;
|
|||
|
|
|
|||
|
|
Person({required this.userId, required this.name, required this.departmentName, required this.phone});
|
|||
|
|
|
|||
|
|
factory Person.fromJson(Map json) {
|
|||
|
|
return Person(
|
|||
|
|
userId: json['id'] ?? '' ,
|
|||
|
|
name: json['username'] ?? '',
|
|||
|
|
departmentName: json['departmentName'] ?? '',
|
|||
|
|
phone: json['phone'] ?? '',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 原回调签名(向后兼容)
|
|||
|
|
typedef PersonSelectCallback = void Function(String userId, String name);
|
|||
|
|
|
|||
|
|
/// 新回调签名,增加可选 index(int,默认 0)
|
|||
|
|
typedef PersonSelectCallbackWithIndex = void Function(String userId, String name, int index);
|
|||
|
|
|
|||
|
|
/// 新增:带 data 的回调(不会替换原有 typedef,向后兼容)
|
|||
|
|
typedef PersonSelectCallbackWithData = void Function(String userId, String name, Map<String, dynamic> data);
|
|||
|
|
|
|||
|
|
/// 新增:带 index 和 data 的回调
|
|||
|
|
typedef PersonSelectCallbackWithIndexAndData = void Function(String userId, String name, int index, Map<String, dynamic> data);
|
|||
|
|
|
|||
|
|
/// 底部弹窗人员选择器(使用预先传入的原始数据列表,不做接口请求)
|
|||
|
|
class DepartmentPersonPicker {
|
|||
|
|
/// 显示人员选择弹窗
|
|||
|
|
///
|
|||
|
|
/// [personsData]: 已拉取并缓存的原始 Map 列表(每项最好是 Map)
|
|||
|
|
/// [onSelected]: 选中后回调 USER_ID 和 NAME(向后兼容旧代码)
|
|||
|
|
/// [onSelectedWithIndex]: 可选的新回调,额外返回 index(index 为在原始 personsData/_all 中的下标,找不到则为 0)
|
|||
|
|
/// [onSelectedWithData]: 可选回调,返回 userId/name + 选中项的完整原始 Map(不影响旧回调)
|
|||
|
|
/// [onSelectedWithIndexWithData]: 可选回调,返回 userId/name/index + 选中项的完整原始 Map(优先级最高)
|
|||
|
|
static Future<void> show(
|
|||
|
|
BuildContext context, {
|
|||
|
|
required List<dynamic> personsData,
|
|||
|
|
PersonSelectCallback? onSelected,
|
|||
|
|
PersonSelectCallbackWithIndex? onSelectedWithIndex,
|
|||
|
|
PersonSelectCallbackWithData? onSelectedWithData,
|
|||
|
|
PersonSelectCallbackWithIndexAndData? onSelectedWithIndexWithData,
|
|||
|
|
}) async {
|
|||
|
|
// 至少传入一个回调(保持对旧调用的兼容)
|
|||
|
|
assert(
|
|||
|
|
onSelected != null ||
|
|||
|
|
onSelectedWithIndex != null ||
|
|||
|
|
onSelectedWithData != null ||
|
|||
|
|
onSelectedWithIndexWithData != null,
|
|||
|
|
'请至少传入一个回调:onSelected / onSelectedWithIndex / onSelectedWithData / onSelectedWithIndexWithData',
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 转换为模型(personsData 可能包含非 Map 的条目)
|
|||
|
|
final List<Person> _all = personsData.map((e) {
|
|||
|
|
if (e is Map) {
|
|||
|
|
return Person.fromJson(e);
|
|||
|
|
} else {
|
|||
|
|
// 非 map 情况按字符串处理
|
|||
|
|
final s = e?.toString() ?? '';
|
|||
|
|
return Person(userId: s, name: s, departmentName: '', phone: '');
|
|||
|
|
}
|
|||
|
|
}).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(() {
|
|||
|
|
if (q.isEmpty) {
|
|||
|
|
_filtered = List.from(_all);
|
|||
|
|
} else {
|
|||
|
|
_filtered = _all.where((p) {
|
|||
|
|
final nameLower = p.name.toLowerCase();
|
|||
|
|
final phoneLower = p.phone.toString().toLowerCase();
|
|||
|
|
return nameLower.contains(q) || phoneLower.contains(q);
|
|||
|
|
}).toList();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据选中的 userId 在原始 personsData 中找到对应的原始 Map(若不存在则生成一个简单 Map)
|
|||
|
|
Map<String, dynamic> _findOriginalData(String userId) {
|
|||
|
|
try {
|
|||
|
|
final idx = personsData.indexWhere((raw) {
|
|||
|
|
if (raw is Map) {
|
|||
|
|
final id = raw['id']?.toString() ?? raw['userId']?.toString() ?? '';
|
|||
|
|
return id == userId;
|
|||
|
|
} else {
|
|||
|
|
return raw?.toString() == userId;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
if (idx >= 0) {
|
|||
|
|
final raw = personsData[idx];
|
|||
|
|
if (raw is Map) return Map<String, dynamic>.from(raw);
|
|||
|
|
return {'id': userId, 'username': _selectedName};
|
|||
|
|
} else {
|
|||
|
|
// 找不到则返回一个最小信息 map
|
|||
|
|
return {'id': userId, 'username': _selectedName};
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
return {'id': userId, 'username': _selectedName};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
// 找到原始 data(Map)
|
|||
|
|
final dataMap = _findOriginalData(_selectedId);
|
|||
|
|
|
|||
|
|
// 优先调用带 index 和 data 的回调(最高优先)
|
|||
|
|
if (onSelectedWithIndexWithData != null) {
|
|||
|
|
onSelectedWithIndexWithData(_selectedId, _selectedName, validIndex, dataMap);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 然后是带 data 的回调(只返回 data,不返回 index)
|
|||
|
|
if (onSelectedWithData != null) {
|
|||
|
|
onSelectedWithData(_selectedId, _selectedName, dataMap);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 然后是带 index 的旧回调
|
|||
|
|
if (onSelectedWithIndex != null) {
|
|||
|
|
onSelectedWithIndex(_selectedId, _selectedName, validIndex);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 最后回退到最原始的回调(仅 userId + name)
|
|||
|
|
if (onSelected != null) {
|
|||
|
|
onSelected(_selectedId, _selectedName);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
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}-${person.phone}(${person.departmentName})'),
|
|||
|
|
trailing: selected ? const Icon(Icons.check, color: Colors.green) : null,
|
|||
|
|
onTap: () => setState(() {
|
|||
|
|
_selectedId = person.userId;
|
|||
|
|
_selectedName = person.name;
|
|||
|
|
}),
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|