262 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			262 lines
		
	
	
		
			7.9 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';
 | ||
| import 'package:qhd_prevention/tools/tools.dart';
 | ||
| 
 | ||
| // ----- Models -----
 | ||
| class Category {
 | ||
|   final String ELECTRONIC_FENCE_AREA_ID;
 | ||
|   final String POSITIONS;
 | ||
|   final String name;
 | ||
|   final List<Category> children;
 | ||
| 
 | ||
|   Category({
 | ||
|     required this.ELECTRONIC_FENCE_AREA_ID,
 | ||
|     required this.name,
 | ||
|     this.POSITIONS = '',
 | ||
|     this.children = const [],
 | ||
|   });
 | ||
| 
 | ||
|   factory Category.fromJson(Map<String, dynamic> json) {
 | ||
|     // 保护式读取,避免 null 转 List、String 时抛异常
 | ||
|     final id = (json['ELECTRONIC_FENCE_AREA_ID'] ?? '').toString();
 | ||
|     final name = (json['name'] ?? '').toString();
 | ||
|     final positions = (json['POSITIONS'] ?? '').toString();
 | ||
| 
 | ||
|     // children 仍然尝试解析(兼容旧结构),但在扁平模式下不会使用
 | ||
|     final rawChildren = json['children'];
 | ||
|     List<Category> childrenList = [];
 | ||
|     if (rawChildren is List) {
 | ||
|       childrenList = rawChildren
 | ||
|           .where((e) => e != null)
 | ||
|           .map((e) {
 | ||
|         if (e is Map<String, dynamic>) {
 | ||
|           return Category.fromJson(e);
 | ||
|         } else if (e is Map) {
 | ||
|           return Category.fromJson(Map<String, dynamic>.from(e));
 | ||
|         } else {
 | ||
|           return null;
 | ||
|         }
 | ||
|       })
 | ||
|           .whereType<Category>()
 | ||
|           .toList();
 | ||
|     }
 | ||
| 
 | ||
|     return Category(
 | ||
|       ELECTRONIC_FENCE_AREA_ID: id,
 | ||
|       name: name,
 | ||
|       POSITIONS: positions,
 | ||
|       children: childrenList,
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| typedef DeptSelectCallback = void Function(String id, String POSITIONS, String name);
 | ||
| 
 | ||
| class WorkAreaPicker extends StatefulWidget {
 | ||
|   final DeptSelectCallback onSelected;
 | ||
| 
 | ||
|   const WorkAreaPicker({Key? key, required this.onSelected}) : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   _WorkAreaPickerState createState() => _WorkAreaPickerState();
 | ||
| }
 | ||
| 
 | ||
| class _WorkAreaPickerState extends State<WorkAreaPicker> {
 | ||
|   String selectedId = '';
 | ||
|   String selectedName = '';
 | ||
|   String selected_POSITIONS = '';
 | ||
| 
 | ||
|   List<Category> original = [];
 | ||
|   List<Category> filtered = [];
 | ||
|   bool loading = true;
 | ||
| 
 | ||
|   final TextEditingController _searchController = TextEditingController();
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     selectedId = '';
 | ||
|     selectedName = '';
 | ||
|     selected_POSITIONS = '';
 | ||
|     _searchController.addListener(_onSearchChanged);
 | ||
|     _loadData();
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     _searchController.removeListener(_onSearchChanged);
 | ||
|     _searchController.dispose();
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   Future<void> _loadData() async {
 | ||
|     try {
 | ||
|       final result = await ApiService.getWorkAreaList();
 | ||
|       List<dynamic> raw = result['varList'] ?? [];
 | ||
| 
 | ||
|       // 解析为 Category 列表(可能包含 children)
 | ||
|       final parsed = raw
 | ||
|           .map((e) {
 | ||
|         if (e is Map<String, dynamic>) return Category.fromJson(e);
 | ||
|         if (e is Map) return Category.fromJson(Map<String, dynamic>.from(e));
 | ||
|         return null;
 | ||
|       })
 | ||
|           .whereType<Category>()
 | ||
|           .toList();
 | ||
| 
 | ||
|       // 扁平化:父与子都放到同一列表中(以兼容后端可能还是两层结构)
 | ||
|       final List<Category> flat = [];
 | ||
|       for (final c in parsed) {
 | ||
|         flat.add(c);
 | ||
|         if (c.children.isNotEmpty) {
 | ||
|           flat.addAll(c.children);
 | ||
|         }
 | ||
|       }
 | ||
| 
 | ||
|       // 规范化 id:如果后端 ID 为空,则为该项赋予一个独一无二的占位 id,
 | ||
|       // 避免与默认 selectedId = '' 冲突导致“全部被选中”的问题。
 | ||
|       final List<Category> normalized = [];
 | ||
|       for (var i = 0; i < flat.length; i++) {
 | ||
|         final c = flat[i];
 | ||
|         final rawId = (c.ELECTRONIC_FENCE_AREA_ID ?? '').toString().trim();
 | ||
|         if (rawId.isEmpty) {
 | ||
|           // 生成占位 id(基于索引和 name hash,足够唯一且不会是空字符串)
 | ||
|           final generatedId = '__generated_${i}_${c.name.hashCode}';
 | ||
|           normalized.add(Category(
 | ||
|             ELECTRONIC_FENCE_AREA_ID: generatedId,
 | ||
|             name: c.name,
 | ||
|             POSITIONS: c.POSITIONS,
 | ||
|             children: const [],
 | ||
|           ));
 | ||
|         } else {
 | ||
|           // 保持原 id
 | ||
|           normalized.add(Category(
 | ||
|             ELECTRONIC_FENCE_AREA_ID: rawId,
 | ||
|             name: c.name,
 | ||
|             POSITIONS: c.POSITIONS,
 | ||
|             children: const [],
 | ||
|           ));
 | ||
|         }
 | ||
|       }
 | ||
| 
 | ||
|       setState(() {
 | ||
|         original = normalized;
 | ||
|         filtered = original;
 | ||
|         loading = false;
 | ||
|       });
 | ||
|     } catch (e, st) {
 | ||
|       debugPrint('WorkAreaPicker._loadData error: $e\n$st');
 | ||
|       setState(() => loading = false);
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   void _onSearchChanged() {
 | ||
|     final query = _searchController.text.toLowerCase().trim();
 | ||
|     setState(() {
 | ||
|       filtered = query.isEmpty ? original : _filterCategories(original, query);
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   // 扁平过滤:在 name 或 POSITIONS 中搜索
 | ||
|   List<Category> _filterCategories(List<Category> list, String query) {
 | ||
|     if (query.isEmpty) return list;
 | ||
|     final q = query.toLowerCase();
 | ||
|     return list.where((c) {
 | ||
|       final name = (c.name ?? '').toLowerCase();
 | ||
|       final pos = (c.POSITIONS ?? '').toLowerCase();
 | ||
|       return name.contains(q) || pos.contains(q);
 | ||
|     }).toList();
 | ||
|   }
 | ||
| 
 | ||
|   Widget _buildRow(Category cat) {
 | ||
|     final isSelected = selectedId == cat.ELECTRONIC_FENCE_AREA_ID;
 | ||
|     return InkWell(
 | ||
|       onTap: () {
 | ||
|         setState(() {
 | ||
|           selectedId = cat.ELECTRONIC_FENCE_AREA_ID;
 | ||
|           selectedName = cat.name;
 | ||
|           selected_POSITIONS = cat.POSITIONS;
 | ||
|         });
 | ||
|       },
 | ||
|       child: Container(
 | ||
|         color: Colors.white,
 | ||
|         padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
 | ||
|         child: Row(
 | ||
|           children: [
 | ||
|             Expanded(child: Text(cat.name)),
 | ||
|             Icon(
 | ||
|               isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
 | ||
|               color: isSelected ? Colors.green : Colors.grey,
 | ||
|             ),
 | ||
|           ],
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     return Container(
 | ||
|       width: MediaQuery.of(context).size.width,
 | ||
|       height: MediaQuery.of(context).size.height * 0.7,
 | ||
|       color: Colors.white,
 | ||
|       child: Column(
 | ||
|         children: [
 | ||
|           Container(
 | ||
|             color: Colors.white,
 | ||
|             padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | ||
|             child: Row(
 | ||
|               children: [
 | ||
|                 GestureDetector(
 | ||
|                   onTap: () => Navigator.of(context).pop(),
 | ||
|                   child: const Text('取消', style: TextStyle(fontSize: 16)),
 | ||
|                 ),
 | ||
|                 Expanded(
 | ||
|                   child: Padding(
 | ||
|                     padding: const EdgeInsets.symmetric(horizontal: 12),
 | ||
|                     child: SearchBarWidget(
 | ||
|                       controller: _searchController,
 | ||
|                       isShowSearchButton: false,
 | ||
|                       onSearch: (keyboard) {},
 | ||
|                     ),
 | ||
|                   ),
 | ||
|                 ),
 | ||
|                 GestureDetector(
 | ||
|                   onTap: () {
 | ||
|                     Navigator.of(context).pop();
 | ||
|                     widget.onSelected(
 | ||
|                       selectedId,
 | ||
|                       selected_POSITIONS,
 | ||
|                       selectedName,
 | ||
|                     );
 | ||
|                   },
 | ||
|                   child: const Text(
 | ||
|                     '确定',
 | ||
|                     style: TextStyle(fontSize: 16, color: Colors.green),
 | ||
|                   ),
 | ||
|                 ),
 | ||
|               ],
 | ||
|             ),
 | ||
|           ),
 | ||
|           const Divider(height: 1),
 | ||
|           Expanded(
 | ||
|             child: loading
 | ||
|                 ? const Center(child: CircularProgressIndicator())
 | ||
|                 : (filtered.isEmpty
 | ||
|                 ? const Center(child: Text('没有找到匹配的工作区域'))
 | ||
|                 : Container(
 | ||
|               color: Colors.white,
 | ||
|               child: ListView.builder(
 | ||
|                 itemCount: filtered.length,
 | ||
|                 itemBuilder: (ctx, idx) => _buildRow(filtered[idx]),
 | ||
|               ),
 | ||
|             )),
 | ||
|           ),
 | ||
|         ],
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| }
 |