466 lines
13 KiB
Dart
466 lines
13 KiB
Dart
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<FirmListPage> createState() => _FirmListPageState();
|
||
}
|
||
|
||
class _FirmListPageState extends State<FirmListPage> {
|
||
List _firmList = []; // 原始数据(全部)
|
||
List _displayList = []; // 经过搜索过滤后用于显示的数据
|
||
|
||
final Map<String, List<Map>> _sections = {};
|
||
final List<String> _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<void> _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<String>.generate(
|
||
26,
|
||
(i) => String.fromCharCode(65 + i),
|
||
);
|
||
final order = <String>[];
|
||
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 = <dynamic>[];
|
||
|
||
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<String>.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<String> get _azIndex {
|
||
final list = List<String>.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 = <Widget>[];
|
||
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();
|
||
}
|
||
}
|