import '../core/utils/dialog_util.dart'; import 'package:flutter/material.dart'; import '../core/responsive/responsive.dart'; import 'package:lpinyin/lpinyin.dart'; import '../core/theme/app_theme.dart'; class OptionItem { final int id; final String name; final String? code; // 拼音索引,构造时预计算 late final String _fullPinyin; late final String _initials; OptionItem({required this.id, required this.name, this.code}) { _fullPinyin = PinyinHelper.getPinyinE(name, separator: '', defPinyin: '').toLowerCase(); _initials = PinyinHelper.getShortPinyin(name).toLowerCase(); } bool matches(String kw) { final k = kw.toLowerCase(); return name.toLowerCase().contains(k) || (_fullPinyin.isNotEmpty && _fullPinyin.contains(k)) || (_initials.isNotEmpty && _initials.contains(k)) || (code?.toLowerCase().contains(k) ?? false); } } /// 点击后弹出搜索对话框的下拉选择框 class SearchableOptionField extends StatelessWidget { final List options; final int? selectedId; final String hint; final String dialogTitle; final ValueChanged onChanged; final bool isRequired; final bool isDense; const SearchableOptionField({ super.key, required this.options, required this.selectedId, required this.hint, required this.dialogTitle, required this.onChanged, this.isRequired = false, this.isDense = true, }); String get _displayText { if (selectedId == null) return ''; return options.where((o) => o.id == selectedId).firstOrNull?.name ?? ''; } Future _openDialog(BuildContext context) async { final result = await showDialog( context: context, builder: (_) => _SearchDialog( title: dialogTitle, options: options, selectedId: selectedId, ), ); // result == -1 means "clear selection" if (result == -1) { onChanged(null); } else if (result != null) { onChanged(result); } } @override Widget build(BuildContext context) { final selected = selectedId != null; return FormField( initialValue: selectedId, validator: isRequired ? (v) => (selectedId == null ? '请选择' : null) : null, builder: (state) { return InkWell( onTap: () => _openDialog(context), borderRadius: BorderRadius.circular(4), child: InputDecorator( decoration: InputDecoration( isDense: isDense, errorText: state.errorText, suffixIcon: selected ? GestureDetector( onTap: () => onChanged(null), child: const Icon(Icons.close, size: 14, color: AppTheme.textSecondary), ) : const Icon(Icons.arrow_drop_down, size: 16, color: AppTheme.textSecondary), ), isEmpty: !selected, child: Text( selected ? _displayText : hint, style: TextStyle( fontSize: 12, color: selected ? Colors.black87 : AppTheme.textSecondary, ), overflow: TextOverflow.ellipsis, ), ), ); }, ); } } class _SearchDialog extends StatefulWidget { final String title; final List options; final int? selectedId; const _SearchDialog({required this.title, required this.options, this.selectedId}); @override State<_SearchDialog> createState() => _SearchDialogState(); } class _SearchDialogState extends State<_SearchDialog> { final _ctrl = TextEditingController(); String _keyword = ''; @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final filtered = _keyword.isEmpty ? widget.options : widget.options.where((o) => o.matches(_keyword)).toList(); return AlertDialog( title: Text(widget.title, style: const TextStyle(fontSize: 16)), contentPadding: const EdgeInsets.fromLTRB(16, 12, 16, 0), content: SizedBox( width: context.dialogWidth(300), height: 400, child: Column( children: [ TextField( controller: _ctrl, autofocus: true, decoration: const InputDecoration( hintText: '搜索...', prefixIcon: Icon(Icons.search, size: 18), isDense: true, ), onChanged: (v) => setState(() => _keyword = v), ), const SizedBox(height: 8), Expanded( child: filtered.isEmpty ? const Center( child: Text('无匹配结果', style: TextStyle(color: AppTheme.textSecondary))) : ListView.builder( itemCount: filtered.length, itemBuilder: (_, i) { final opt = filtered[i]; final isSelected = opt.id == widget.selectedId; return ListTile( dense: true, title: Text(opt.name, style: const TextStyle(fontSize: 13)), subtitle: opt.code != null && opt.code!.isNotEmpty ? Text(opt.code!, style: const TextStyle(fontSize: 11)) : null, selected: isSelected, selectedTileColor: AppTheme.primary.withValues(alpha: 0.08), onTap: () => Navigator.of(context).pop(opt.id), ); }, ), ), ], ), ), actions: [ if (widget.selectedId != null) TextButton( onPressed: () => Navigator.of(context).pop(-1), child: const Text('清除选择', style: TextStyle(color: AppTheme.textSecondary)), ), TextButton( onPressed: () => Navigator.of(context).pop(null), child: const Text('取消'), ), ], ); } }