510 lines
14 KiB
Dart
510 lines
14 KiB
Dart
import 'dart:convert';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:qhd_prevention/customWidget/search_bar_widget.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 hiddenregionId;
|
||
final String hiddenregion;
|
||
final String? responsibleDeptName;
|
||
final String? responsibleDeptId;
|
||
final String? responsibleUserName;
|
||
final String? responsibleUserId;
|
||
final int sortindex;
|
||
final String? comments;
|
||
final String? corpinfoId;
|
||
final String? sbdl;
|
||
final String? sbmc;
|
||
final List<DictCategory>? children;
|
||
|
||
DictCategory({
|
||
required this.id,
|
||
required this.name,
|
||
required this.extValues,
|
||
required this.parentId,
|
||
required this.hiddenregionId,
|
||
required this.hiddenregion,
|
||
this.responsibleDeptName,
|
||
this.responsibleDeptId,
|
||
this.responsibleUserName,
|
||
this.responsibleUserId,
|
||
required this.sortindex,
|
||
this.comments,
|
||
this.corpinfoId,
|
||
this.sbdl,
|
||
this.sbmc,
|
||
this.children,
|
||
});
|
||
|
||
factory DictCategory.fromJson(Map<String, dynamic> json) {
|
||
// 安全读取并兼容字符串或数字类型的 id
|
||
String parseString(dynamic v) {
|
||
if (v == null) return '';
|
||
if (v is String) return v;
|
||
return v.toString();
|
||
}
|
||
|
||
int parseInt(dynamic v) {
|
||
if (v == null) return 0;
|
||
if (v is int) return v;
|
||
if (v is String) return int.tryParse(v) ?? 0;
|
||
return 0;
|
||
}
|
||
|
||
// 处理子节点
|
||
final rawChildren = json['children'];
|
||
List<DictCategory>? childrenList;
|
||
if (rawChildren is List && rawChildren.isNotEmpty) {
|
||
try {
|
||
childrenList = rawChildren
|
||
.where((e) => e != null)
|
||
.map((e) => DictCategory.fromJson(Map<String, dynamic>.from(e as Map)))
|
||
.toList();
|
||
} catch (e) {
|
||
childrenList = null;
|
||
}
|
||
}
|
||
|
||
// 处理扩展值
|
||
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['hiddenregion']), // 使用 hiddenregion 作为显示名称
|
||
extValues: extMap,
|
||
parentId: parseString(json['parentId']),
|
||
hiddenregionId: parseString(json['hiddenregionId']),
|
||
hiddenregion: parseString(json['hiddenregion']),
|
||
responsibleDeptName: parseString(json['responsibleDeptName']),
|
||
responsibleDeptId: parseString(json['responsibleDeptId']),
|
||
responsibleUserName: parseString(json['responsibleUserName']),
|
||
responsibleUserId: parseString(json['responsibleUserId']),
|
||
sortindex: parseInt(json['sortindex']),
|
||
comments: parseString(json['comments']),
|
||
corpinfoId: parseString(json['corpinfoId']),
|
||
sbdl: parseString(json['sbdl']),
|
||
sbmc: parseString(json['sbmc']),
|
||
children: childrenList,
|
||
);
|
||
}
|
||
|
||
// 转换为Map,便于使用
|
||
Map<String, dynamic> toMap() {
|
||
return {
|
||
'id': id,
|
||
'name': name,
|
||
'hiddenregionId': hiddenregionId,
|
||
'hiddenregion': hiddenregion,
|
||
'parentId': parentId,
|
||
'responsibleDeptName': responsibleDeptName,
|
||
'responsibleDeptId': responsibleDeptId,
|
||
'responsibleUserName': responsibleUserName,
|
||
'responsibleUserId': responsibleUserId,
|
||
'sortindex': sortindex,
|
||
'comments': comments,
|
||
'corpinfoId': corpinfoId,
|
||
'sbdl': sbdl,
|
||
'sbmc': sbmc,
|
||
'extValues': extValues,
|
||
};
|
||
}
|
||
}
|
||
|
||
/// 数据字典选择器回调签名
|
||
typedef DictSelectCallback = void Function(String id, String name, Map<String, dynamic>? extraData);
|
||
|
||
class DangerPartsPicker extends StatefulWidget {
|
||
|
||
/// 回调,返回选中项的 id, name 和额外数据
|
||
final DictSelectCallback onSelected;
|
||
|
||
/// 是否显示搜索框
|
||
final bool showSearch;
|
||
|
||
/// 标题
|
||
final String title;
|
||
|
||
/// 确认按钮文本
|
||
final String confirmText;
|
||
|
||
/// 取消按钮文本
|
||
final String cancelText;
|
||
|
||
const DangerPartsPicker({
|
||
Key? key,
|
||
required this.onSelected,
|
||
this.showSearch = true,
|
||
this.title = '请选择',
|
||
this.confirmText = '确定',
|
||
this.cancelText = '取消',
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
_DangerPartsPickerState createState() => _DangerPartsPickerState();
|
||
}
|
||
|
||
class _DangerPartsPickerState extends State<DangerPartsPicker> {
|
||
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 = '';
|
||
|
||
final TextEditingController _searchController = TextEditingController();
|
||
|
||
@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 HiddenDangerApi.getHiddenDangerAreas();
|
||
final raw = result['data'] as List<dynamic>;
|
||
setState(() {
|
||
original = raw.map((e) => DictCategory.fromJson(e as Map<String, dynamic>)).toList();
|
||
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 != null && 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) {
|
||
List<DictCategory>? children;
|
||
if (cat.children != null) {
|
||
children = _filterCategories(cat.children!, query);
|
||
}
|
||
|
||
if (cat.name.toLowerCase().contains(query) ||
|
||
(children != null && children.isNotEmpty)) {
|
||
result.add(
|
||
DictCategory(
|
||
id: cat.id,
|
||
name: cat.name,
|
||
children: children,
|
||
extValues: cat.extValues,
|
||
parentId: cat.parentId,
|
||
hiddenregionId: cat.hiddenregionId,
|
||
hiddenregion: cat.hiddenregion,
|
||
responsibleDeptName: cat.responsibleDeptName,
|
||
responsibleDeptId: cat.responsibleDeptId,
|
||
responsibleUserName: cat.responsibleUserName,
|
||
responsibleUserId: cat.responsibleUserId,
|
||
sortindex: cat.sortindex,
|
||
comments: cat.comments,
|
||
corpinfoId: cat.corpinfoId,
|
||
sbdl: cat.sbdl,
|
||
sbmc: cat.sbmc,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
Widget _buildRow(DictCategory category, int indent) {
|
||
final hasChildren = category.children != null && category.children!.isNotEmpty;
|
||
final isExpanded = expandedSet.contains(category.id);
|
||
final isSelected = category.id == selectedId;
|
||
|
||
return Column(
|
||
children: [
|
||
InkWell(
|
||
onTap: () {
|
||
setState(() {
|
||
if (hasChildren) {
|
||
isExpanded
|
||
? expandedSet.remove(category.id)
|
||
: expandedSet.add(category.id);
|
||
}
|
||
selectedId = category.id;
|
||
selectedName = category.name;
|
||
selectedExtraData = category.toMap();
|
||
});
|
||
},
|
||
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: Icon(
|
||
isSelected
|
||
? Icons.radio_button_checked
|
||
: Icons.radio_button_unchecked,
|
||
color: Colors.blue,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
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
|
||
: () {
|
||
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),
|
||
ElevatedButton(
|
||
onPressed: _loadDictData,
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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),
|
||
),
|
||
);
|
||
}
|
||
|
||
@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: [
|
||
// 标题行
|
||
// _buildTitleBar(),
|
||
// 操作行(取消、搜索、确定)
|
||
_buildActionBar(),
|
||
const Divider(height: 1),
|
||
|
||
// 内容区域
|
||
Expanded(
|
||
child: _buildContent(),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
} |