QinGang_interested/lib/customWidget/MultiDictValuesPicker.dart

522 lines
16 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.

import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/search_bar_widget.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/http/modules/basic_info_api.dart';
import 'package:qhd_prevention/http/ApiService.dart';
// 数据字典模型
class DictCategory {
final String id;
final String name;
final Map<String, dynamic> extValues;
final String parentId;
final String dictValue;
final String dictLabel;
final List<DictCategory> children;
late final String dingValue;
late final String dingName;
DictCategory({
required this.id,
required this.name,
required this.children,
required this.extValues,
required this.parentId,
required this.dictValue,
required this.dictLabel,
required this.dingValue,
required this.dingName,
});
factory DictCategory.fromJson(Map<String, dynamic> json) {
String parseString(dynamic v) {
if (v == null) return '';
if (v is String) return v;
return v.toString();
}
final rawChildren = json['children'];
List<DictCategory> childrenList = [];
if (rawChildren is List) {
try {
childrenList = rawChildren
.where((e) => e != null)
.map((e) => DictCategory.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
} catch (e) {
childrenList = [];
}
}
final extRaw = json['extValues'];
Map<String, dynamic> extMap = {};
if (extRaw is Map) {
extMap = Map<String, dynamic>.from(extRaw);
}
return DictCategory(
id: parseString(json['id']),
name: parseString(json['dictLabel'] ?? json['name']),
children: childrenList,
extValues: extMap,
parentId: parseString(json['parentId']),
dictValue: parseString(json['dictValue']),
dictLabel: parseString(json['dictLabel'] ?? json['name']),
dingValue: "",
dingName: "",
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'dictValue': dictValue,
'dictLabel': dictLabel,
'parentId': parentId,
'extValues': extValues,
};
}
}
typedef DictSelectCallback = void Function(String id, String name, Map<String, dynamic>? extraData);
class MultiDictValuesPicker extends StatefulWidget {
final String dictType;
final DictSelectCallback onSelected;
final bool showSearch;
final String title;
final String confirmText;
final String cancelText;
final bool showIgnoreHidden;
final bool isSafeChekDangerLevel;
final bool allowSelectParent;
const MultiDictValuesPicker({
Key? key,
required this.dictType,
required this.onSelected,
this.showSearch = true,
this.title = '请选择',
this.confirmText = '确定',
this.cancelText = '取消',
this.isSafeChekDangerLevel = false,
this.showIgnoreHidden = false,
this.allowSelectParent = true,
}) : super(key: key);
@override
_MultiDictValuesPickerState createState() => _MultiDictValuesPickerState();
}
class _MultiDictValuesPickerState extends State<MultiDictValuesPicker> {
String selectedId = '';
String selectedName = '';
Map<String, dynamic>? selectedExtraData;
Set<String> expandedSet = {};
List<DictCategory> original = [];
List<DictCategory> filtered = [];
bool loading = true;
bool error = false;
String errorMessage = '';
String dingValueWai = '';
String dingNameWai = '';
final TextEditingController _searchController = TextEditingController();
// id -> node 映射,用于快速查父节点
final Map<String, DictCategory> _idMap = {};
@override
void initState() {
super.initState();
selectedId = '';
selectedName = '';
expandedSet = {};
_searchController.addListener(_onSearchChanged);
_loadDictData();
}
@override
void dispose() {
_searchController.removeListener(_onSearchChanged);
_searchController.dispose();
super.dispose();
}
Future<void> _loadDictData() async {
try {
setState(() {
loading = true;
error = false;
});
final result = await BasicInfoApi.getDictValues(widget.dictType);
List<dynamic> raw = result['data'] as List<dynamic>;
if (!widget.showIgnoreHidden) {
if (widget.dictType == 'hiddenLevel') {
for (int i = 0; i < raw.length; i++) {
if (raw[i]['children'] != null) {
raw[i]['children'].removeWhere((child) => child != null && child['dictLabel'] == '忽略隐患');
}
}
}
}
late List<dynamic> resultRows = [];
if (widget.isSafeChekDangerLevel) {
if (widget.dictType == 'hiddenLevel') {
for (int i = 0; i < raw.length; i++) {
if (raw[i]['children'] != null) {
for (int m = 0; m < raw[i]['children'].length; m++) {
final ch = raw[i]['children'][m];
if (ch != null && (ch['dictLabel'] == '轻微隐患' || ch['dictLabel'] == '一般隐患')) {
resultRows.add(ch);
}
}
}
}
} else {
resultRows = raw;
}
} else {
resultRows = raw;
}
// 转换为 DictCategory 列表
final loaded = resultRows.map((e) => DictCategory.fromJson(e as Map<String, dynamic>)).toList();
// 构建 id map包含所有节点的扁平映射
_idMap.clear();
void buildIdMap(List<DictCategory> nodes) {
for (final n in nodes) {
_idMap[n.id] = n;
if (n.children.isNotEmpty) {
buildIdMap(n.children);
}
}
}
buildIdMap(loaded);
setState(() {
original = loaded;
filtered = original;
loading = false;
});
} catch (e) {
setState(() {
loading = false;
error = true;
errorMessage = e.toString();
});
}
}
void _onSearchChanged() {
final query = _searchController.text.toLowerCase().trim();
setState(() {
filtered = query.isEmpty ? original : _filterCategories(original, query);
if (query.isNotEmpty) {
expandedSet.addAll(_getAllExpandableIds(filtered));
}
});
}
Set<String> _getAllExpandableIds(List<DictCategory> categories) {
Set<String> ids = {};
for (var category in categories) {
if (category.children.isNotEmpty) {
ids.add(category.id);
ids.addAll(_getAllExpandableIds(category.children));
}
}
return ids;
}
List<DictCategory> _filterCategories(List<DictCategory> list, String query) {
List<DictCategory> result = [];
for (var cat in list) {
final children = _filterCategories(cat.children, query);
if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) {
result.add(
DictCategory(
id: cat.id,
name: cat.name,
children: children,
extValues: cat.extValues,
parentId: cat.parentId,
dictValue: cat.dictValue,
dictLabel: cat.dictLabel,
dingValue: "",
dingName: "",
),
);
}
}
return result;
}
// 判断节点是否可选
bool _isSelectableNode(DictCategory category) {
if (widget.allowSelectParent) {
return true;
}
return category.children.isEmpty;
}
// 查找某个 id 的最上层祖先节点(若自身就是顶层则返回自身)
DictCategory? _getTopAncestor(String id) {
if (id.isEmpty) return null;
DictCategory? current = _idMap[id];
if (current == null) return null;
// 如果没有 parentId 或 parentId 为空,当前即为顶层
while (current != null && current.parentId.isNotEmpty) {
final parent = _idMap[current.parentId];
if (parent == null) break;
current = parent;
}
return current;
}
Widget _buildRow(DictCategory category, int indent) {
final hasChildren = category.children.isNotEmpty;
final isExpanded = expandedSet.contains(category.id);
final isSelected = category.id == selectedId;
final isSelectable = _isSelectableNode(category);
return Column(
children: [
InkWell(
onTap: () {
setState(() {
if (hasChildren) {
isExpanded ? expandedSet.remove(category.id) : expandedSet.add(category.id);
}
if (isSelectable) {
selectedId = category.id;
selectedName = category.name;
selectedExtraData = category.toMap();
// 计算最上层父节点(如果存在)
final top = _getTopAncestor(category.id);
if (top != null) {
dingValueWai = top.dictValue;
dingNameWai = top.name;
} else {
// 回退到自身值(防御)
dingValueWai = category.dictValue;
dingNameWai = category.name;
}
}
});
},
child: Container(
color: Colors.white,
child: Row(
children: [
SizedBox(width: 16.0 * indent),
SizedBox(
width: 24,
child: hasChildren
? Icon(
isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_right_rounded,
size: 35,
color: Colors.grey[600],
)
: const SizedBox.shrink(),
),
const SizedBox(width: 5),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category.name,
style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: Colors.black,
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: isSelectable
? Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
color: Colors.blue,
)
: null,
),
],
),
),
),
if (hasChildren && isExpanded) ...category.children.map((child) => _buildRow(child, indent + 1)),
],
);
}
Widget _buildTitleBar() {
return Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Center(
child: Text(
widget.title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
);
}
Widget _buildActionBar() {
return Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text(widget.cancelText, style: const TextStyle(fontSize: 16, color: Colors.grey)),
),
),
if (widget.showSearch) ...[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SearchBarWidget(controller: _searchController, isShowSearchButton: false, onSearch: (keyboard) {}),
),
),
] else
const Expanded(child: SizedBox()),
GestureDetector(
onTap: selectedId.isEmpty
? null
: () {
// 在返回之前,将最上层父节点信息也回传(保证 selectedExtraData 存在)
final top = _getTopAncestor(selectedId);
if (top != null) {
selectedExtraData ??= {};
selectedExtraData!['dingValue'] = top.dictValue;
selectedExtraData!['dingName'] = top.name;
} else {
selectedExtraData ??= {};
selectedExtraData!['dingValue'] = dingValueWai;
selectedExtraData!['dingName'] = dingNameWai;
}
Navigator.of(context).pop();
widget.onSelected(selectedId, selectedName, selectedExtraData);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text(
widget.confirmText,
style: TextStyle(
fontSize: 16,
color: selectedId.isEmpty ? Colors.grey : Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
Widget _buildContent() {
if (loading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator(), SizedBox(height: 16), Text('加载中...')],
),
);
}
if (error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
const Text('加载失败', style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text(errorMessage, style: const TextStyle(fontSize: 12, color: Colors.grey), textAlign: TextAlign.center),
const SizedBox(height: 16),
CustomButton(text: '重试', onPressed: _loadDictData),
],
),
);
}
if (filtered.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(Icons.search_off, size: 48, color: Colors.grey), SizedBox(height: 16), Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey))],
),
);
}
return Container(
color: Colors.white,
child: ListView.builder(
itemCount: filtered.length,
itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0),
),
);
}
Widget _buildHintMessage() {
if (!widget.allowSelectParent) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.blue[50],
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: Colors.blue[700]),
const SizedBox(width: 8),
Expanded(child: Text('只能选择没有子项的节点', style: TextStyle(fontSize: 12, color: Colors.blue[700]))),
],
),
);
}
return const SizedBox.shrink();
}
@override
Widget build(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.7,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, -2))],
),
child: Column(
children: [
_buildActionBar(),
const Divider(height: 1),
Expanded(child: _buildContent()),
],
),
);
}
}