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/pages/mine/onboarding_full_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; // lpinyin 用于中文转拼音 import 'package:lpinyin/lpinyin.dart'; import 'package:qhd_prevention/tools/tools.dart'; class FirmListPage extends StatefulWidget { const FirmListPage({super.key}); @override State createState() => _FirmListPageState(); } class _FirmListPageState extends State { List _firmList = []; // 原始数据(全部) List _displayList = []; // 经过搜索过滤后用于显示的数据 final Map> _sections = {}; final List _sectionOrder = []; final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); static const double headerHeight = 40.0; static const double itemHeight = 64.0; @override void initState() { super.initState(); _getFirmList(); } Future _getFirmList() async { try { LoadingDialogHelper.show(); final result = await BasicInfoApi.getFirmList({'enterpriseType': 3}); LoadingDialogHelper.hide(); if (result['success'] == true && result['data'] is List) { setState(() { _firmList = List.from(result['data']); _displayList = List.from(_firmList); }); _groupAndSort(); } else { setState(() { _firmList = []; _displayList = []; _sections.clear(); _sectionOrder.clear(); }); } } catch (e) { // 处理异常 setState(() { _firmList = []; _displayList = []; _sections.clear(); _sectionOrder.clear(); }); } } // 取企业显示名(优先字段) String _firmName(Map m) { final keys = [ 'corpName', 'companyName', 'name', 'firmName', 'title', 'epcProjectName', ]; for (final k in keys) { if (m.containsKey(k) && m[k] != null && m[k].toString().trim().isNotEmpty) { return m[k].toString().trim(); } } // 若没有合适字段,尝试取第一个非空 value for (final entry in m.entries) { final v = entry.value; if (v is String && v.trim().isNotEmpty) return v.trim(); if (v is num) return v.toString(); } return '未命名企业'; } // 将 displayList 按拼音首字母分组并排序 void _groupAndSort() { _sections.clear(); for (final raw in _displayList) { if (raw is! Map) continue; final name = _firmName(raw); String shortPinyin = ''; try { shortPinyin = PinyinHelper.getShortPinyin(name); } catch (_) { shortPinyin = ''; } final firstLetter = (shortPinyin.isNotEmpty ? shortPinyin[0].toUpperCase() : (name.isNotEmpty ? name[0].toUpperCase() : '#')); final letter = RegExp(r'^[A-Z]$').hasMatch(firstLetter) ? firstLetter : '#'; _sections.putIfAbsent(letter, () => []).add(raw); } // 对每个分组内部按拼音全拼排序(不带声调) for (final k in _sections.keys) { _sections[k]!.sort((a, b) { final na = _firmName(a); final nb = _firmName(b); String pa; String pb; try { pa = PinyinHelper.getPinyin( na, separator: '', format: PinyinFormat.WITHOUT_TONE, ).toLowerCase(); } catch (_) { pa = na.toLowerCase(); } try { pb = PinyinHelper.getPinyin( nb, separator: '', format: PinyinFormat.WITHOUT_TONE, ).toLowerCase(); } catch (_) { pb = nb.toLowerCase(); } return pa.compareTo(pb); }); } // 构建分组顺序:A..Z,然后 '#' final letters = List.generate( 26, (i) => String.fromCharCode(65 + i), ); final order = []; for (final l in letters) { if (_sections.containsKey(l)) order.add(l); } if (_sections.containsKey('#')) order.add('#'); setState(() { _sectionOrder ..clear() ..addAll(order); }); } // 过滤函数:支持中文直接匹配,也支持拼音匹配 void _applyFilter(String q) { final query = q.trim(); if (query.isEmpty) { setState(() { _displayList = List.from(_firmList); }); _groupAndSort(); return; } final qLower = query.toLowerCase(); final filtered = []; for (final raw in _firmList) { if (raw is! Map) continue; final name = _firmName(raw); final nameLower = name.toLowerCase(); bool matched = false; // 1) 中文/英文直接包含 if (nameLower.contains(qLower)) matched = true; // 2) 拼音匹配 if (!matched) { try { final pinyin = PinyinHelper.getPinyin( name, separator: '', format: PinyinFormat.WITHOUT_TONE, ).toLowerCase(); if (pinyin.contains(qLower)) matched = true; } catch (_) { // ignore } } if (matched) filtered.add(raw); } setState(() { _displayList = filtered; }); _groupAndSort(); } // 计算某个分组的列表起始偏移(基于 headerHeight/itemHeight 的估算) double _offsetForSection(String letter) { double offset = 0.0; for (final l in _sectionOrder) { if (l == letter) break; offset += headerHeight; final cnt = _sections[l]?.length ?? 0; offset += cnt * itemHeight; } return offset; } void _jumpToLetter(String letter) { if (_sectionOrder.isEmpty) return; if (_sections.containsKey(letter)) { final off = _offsetForSection(letter); _scrollController.animateTo( off, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, ); return; } final all = List.generate(26, (i) => String.fromCharCode(65 + i)) + ['#']; final idx = all.indexOf(letter); if (idx == -1) return; for (int i = idx + 1; i < all.length; i++) { final l = all[i]; if (_sections.containsKey(l)) { final off = _offsetForSection(l); _scrollController.animateTo( off, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, ); return; } } final last = _sectionOrder.last; final off = _offsetForSection(last); _scrollController.animateTo( off, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, ); } List get _azIndex { final list = List.generate(26, (i) => String.fromCharCode(65 + i)); list.add('#'); return list; } @override Widget build(BuildContext context) { // 右侧字母索引(灰色圆角背景),使用固定高度以忽略键盘导致的可用高度变化 final mq = MediaQuery.of(context); final fixedIndexHeight = mq.size.height - kToolbarHeight - mq.padding.top - 24-100; return Scaffold( backgroundColor: Colors.white, appBar: MyAppbar(title: '选择企业'), body: SafeArea( child: Column( children: [ // 搜索框(固定在顶部) Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: SearchBarWidget( onSearch: (keyboard) {}, controller: _searchController, isShowSearchButton: false, resetButtonText: '重置', showResetButton: true, onTextChanged: (text) { _applyFilter(text); }, onReset: () { _searchController.clear(); _applyFilter(''); }, ), ), // 列表区域 Expanded( child: Stack( children: [ // 列表区域:RefreshIndicator 必须包裹可滚动控件(ListView) Padding( padding: const EdgeInsets.only(right: 48.0), // 给右侧字母索引留空间 child: _buildListView(), ), // 24 是上/下 margin 的大致预留(12 + 12),根据你视觉需要可调 Positioned( right: 8, top: 12, // 不使用 bottom(避免随键盘收缩),改为固定高度(独立于 viewInsets) height: fixedIndexHeight, child: _buildAlphabetIndex(fixedIndexHeight), ), ], ), ), ], ), ), ); } Widget _buildListView() { if (_sectionOrder.isEmpty) { return ListView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), children: [ const SizedBox(height: 30), Center( child: Text( _displayList.isEmpty ? ' 暂无企业' : ' 正在加载…', style: TextStyle(fontSize: 15, color: Colors.grey[600]), ), ), ], ); } final children = []; for (final letter in _sectionOrder) { children.add(_buildSectionHeader(letter)); final items = _sections[letter] ?? []; for (final item in items) { children.add(_buildItem(item)); } } return ListView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(vertical: 0), children: children, ); } Widget _buildSectionHeader(String letter) { return Container( height: headerHeight, padding: const EdgeInsets.symmetric(horizontal: 15), color: Colors.grey.shade100, alignment: Alignment.centerLeft, child: Text( letter, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ); } Widget _buildItem(Map firm) { final name = _firmName(firm); return InkWell( onTap: () { pushPage(OnboardingFullPage(scanData: firm), context); }, child: Container( height: itemHeight, padding: const EdgeInsets.symmetric(horizontal: 15), color: Colors.white, child: Row( children: [ Expanded( child: Text( name, style: const TextStyle(fontSize: 15), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } Widget _buildAlphabetIndex(double height) { final letters = _azIndex; return SizedBox( width: 20, height: height, child: Container( // 背景圆角盒子占满高度 padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 4), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(10), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0, 2), ), ], ), child: Column( // 等间隔排列字母,使它们在固定高度内均匀分布,不会被键盘压缩 mainAxisAlignment: MainAxisAlignment.spaceBetween, children: letters.map((l) { final enabled = _sections.containsKey(l); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _jumpToLetter(l), child: Container( // 尽量让每个字母区域可点击,并根据启用状态改变样式 padding: const EdgeInsets.symmetric( vertical: 2, horizontal: 4, ), alignment: Alignment.center, child: Text( l, style: TextStyle( fontSize: 11, color: enabled ? Colors.blue : Colors.grey.shade400, fontWeight: FontWeight.w500, ), ), ), ); }).toList(), ), ), ); } @override void dispose() { _scrollController.dispose(); _searchController.dispose(); super.dispose(); } }