2025-12-12 09:11:30 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/customWidget/search_bar_widget.dart';
|
2026-02-28 14:38:07 +08:00
|
|
|
|
import 'package:qhd_prevention/customWidget/custom_button.dart';
|
|
|
|
|
|
import 'package:qhd_prevention/http/modules/basic_info_api.dart';
|
2025-12-12 09:11:30 +08:00
|
|
|
|
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']),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
name: parseString(json['dictLabel'] ?? json['name']),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
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;
|
2026-02-28 14:38:07 +08:00
|
|
|
|
final bool showIgnoreHidden;
|
|
|
|
|
|
final bool isSafeChekDangerLevel;
|
|
|
|
|
|
final bool allowSelectParent;
|
2025-12-12 09:11:30 +08:00
|
|
|
|
|
|
|
|
|
|
const MultiDictValuesPicker({
|
|
|
|
|
|
Key? key,
|
|
|
|
|
|
required this.dictType,
|
|
|
|
|
|
required this.onSelected,
|
|
|
|
|
|
this.showSearch = true,
|
|
|
|
|
|
this.title = '请选择',
|
|
|
|
|
|
this.confirmText = '确定',
|
|
|
|
|
|
this.cancelText = '取消',
|
2026-02-28 14:38:07 +08:00
|
|
|
|
this.isSafeChekDangerLevel = false,
|
|
|
|
|
|
this.showIgnoreHidden = false,
|
|
|
|
|
|
this.allowSelectParent = true,
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}) : 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();
|
|
|
|
|
|
|
2026-02-28 14:38:07 +08:00
|
|
|
|
// id -> node 映射,用于快速查父节点
|
|
|
|
|
|
final Map<String, DictCategory> _idMap = {};
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
@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);
|
2026-02-28 14:38:07 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
setState(() {
|
2026-02-28 14:38:07 +08:00
|
|
|
|
original = loaded;
|
2025-12-12 09:11:30 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 14:38:07 +08:00
|
|
|
|
// 判断节点是否可选
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
Widget _buildRow(DictCategory category, int indent) {
|
|
|
|
|
|
final hasChildren = category.children.isNotEmpty;
|
|
|
|
|
|
final isExpanded = expandedSet.contains(category.id);
|
|
|
|
|
|
final isSelected = category.id == selectedId;
|
2026-02-28 14:38:07 +08:00
|
|
|
|
final isSelectable = _isSelectableNode(category);
|
2025-12-12 09:11:30 +08:00
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
InkWell(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
if (hasChildren) {
|
2026-02-28 14:38:07 +08:00
|
|
|
|
isExpanded ? expandedSet.remove(category.id) : expandedSet.add(category.id);
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
2026-02-28 14:38:07 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-12-12 09:11:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(width: 16.0 * indent),
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 24,
|
|
|
|
|
|
child: hasChildren
|
|
|
|
|
|
? Icon(
|
2026-02-28 14:38:07 +08:00
|
|
|
|
isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_right_rounded,
|
2025-12-12 09:11:30 +08:00
|
|
|
|
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),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
child: isSelectable
|
|
|
|
|
|
? Icon(
|
|
|
|
|
|
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
|
2025-12-12 09:11:30 +08:00
|
|
|
|
color: Colors.blue,
|
2026-02-28 14:38:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
: null,
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
if (hasChildren && isExpanded) ...category.children.map((child) => _buildRow(child, indent + 1)),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildTitleBar() {
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
|
|
|
|
child: Center(
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
widget.title,
|
2026-02-28 14:38:07 +08:00
|
|
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
child: Text(widget.cancelText, style: const TextStyle(fontSize: 16, color: Colors.grey)),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (widget.showSearch) ...[
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
child: SearchBarWidget(controller: _searchController, isShowSearchButton: false, onSearch: (keyboard) {}),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
] else
|
2025-12-12 09:11:30 +08:00
|
|
|
|
const Expanded(child: SizedBox()),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: selectedId.isEmpty
|
|
|
|
|
|
? null
|
|
|
|
|
|
: () {
|
2026-02-28 14:38:07 +08:00
|
|
|
|
// 在返回之前,将最上层父节点信息也回传(保证 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
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,
|
2026-02-28 14:38:07 +08:00
|
|
|
|
children: [CircularProgressIndicator(), SizedBox(height: 16), Text('加载中...')],
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
Text(errorMessage, style: const TextStyle(fontSize: 12, color: Colors.grey), textAlign: TextAlign.center),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
const SizedBox(height: 16),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
CustomButton(text: '重试', onPressed: _loadDictData),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filtered.isEmpty) {
|
|
|
|
|
|
return const Center(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2026-02-28 14:38:07 +08:00
|
|
|
|
children: [Icon(Icons.search_off, size: 48, color: Colors.grey), SizedBox(height: 16), Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey))],
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
child: ListView.builder(
|
|
|
|
|
|
itemCount: filtered.length,
|
|
|
|
|
|
itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 14:38:07 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 09:11:30 +08:00
|
|
|
|
@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,
|
2026-02-28 14:38:07 +08:00
|
|
|
|
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))],
|
2025-12-12 09:11:30 +08:00
|
|
|
|
),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_buildActionBar(),
|
|
|
|
|
|
const Divider(height: 1),
|
2026-02-28 14:38:07 +08:00
|
|
|
|
Expanded(child: _buildContent()),
|
2025-12-12 09:11:30 +08:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-28 14:38:07 +08:00
|
|
|
|
}
|