QinGang_interested/lib/pages/user/firm_list_page.dart

466 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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();
}
}