import 'package:flutter/material.dart'; import '../../api/oa_client.dart'; import '../../im/pinyin.dart'; import 'desk_org_browse_page.dart'; import '../../session/session.dart'; import '../desk_theme.dart'; import '../widgets/desk_widgets.dart'; class DeskContactsPage extends StatefulWidget { const DeskContactsPage({super.key, required this.session, required this.api, this.onMessage}); final SessionStore session; final OaClient api; final void Function(Map person)? onMessage; @override State createState() => _DeskContactsPageState(); } class _DeskContactsPageState extends State { List> _items = []; Map> _presence = {}; String _q = ''; bool _loading = true; String? _selectedId; @override void initState() { super.initState(); _load(); } Future _load() async { try { final data = await widget.api.get('/staff'); Map> presence = {}; try { final ids = asMaps(data).map((e) => '${e['id']}').where((e) => e.isNotEmpty).join(','); final raw = await widget.api.get('/im/presence', query: {'userIds': ids}); if (raw is Map) { presence = raw.map((k, v) => MapEntry('$k', (v is List ? v : const []).map((x) => '$x').toList())); } } catch (_) {} if (!mounted) return; setState(() { _items = asMaps(data); _presence = presence; _loading = false; }); } catch (_) { if (mounted) setState(() => _loading = false); } } List> get _shown { if (_q.isEmpty) return _items; return _items.where((e) => '${e['name']}${e['mobile']}${e['department']}'.contains(_q)).toList(); } Map>> get _groups { final g = >>{}; for (final r in _shown) { final letter = letterOf('${r['name'] ?? ''}'); g.putIfAbsent(letter, () => []).add(r); } for (final k in g.keys) { g[k]!.sort((a, b) => '${a['name']}'.compareTo('${b['name']}')); } return g; } Future _message(Map r) async { if (widget.onMessage != null) { widget.onMessage!(r); return; } String conversationId = ''; try { final raw = await widget.api.post('/im/conversations/direct', {'peerId': '${r['id']}'}); if (raw is Map) conversationId = '${raw['conversationId'] ?? raw['id'] ?? ''}'; } catch (_) {} if (!mounted) return; widget.onMessage?.call({ ...r, 'conversationId': conversationId, 'peerId': '${r['id']}', 'name': '${r['name'] ?? '同事'}', 'type': 'direct', }); } @override Widget build(BuildContext context) { final groups = _groups; final letters = groups.keys.toList()..sort(); final depts = _items.map((e) => '${e['department'] ?? ''}').where((e) => e.isNotEmpty).toSet(); final selected = _selectedId == null ? null : _items.cast?>().firstWhere( (e) => '${e?['id']}' == _selectedId, orElse: () => null, ); return ColoredBox( color: kDeskBg, child: Row( children: [ SizedBox( width: 300, child: ColoredBox( color: kDeskListBg, child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: DeskSearchField(hint: '搜索同事', onChanged: (v) => setState(() => _q = v.trim())), ), if (_loading) const LinearProgressIndicator(minHeight: 2, color: kDeskBind), Expanded( child: ListView( children: [ if (_q.isEmpty) ...[ _entry(Icons.account_tree, '组织架构', '${depts.length} 个部门', () { Navigator.of(context).push(MaterialPageRoute( builder: (_) => DeskOrgBrowsePage( session: widget.session, api: widget.api, staff: _items, onMessage: widget.onMessage, ), )); }), const Divider(height: 1, color: kDeskLine), ], for (final letter in letters) ...[ Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 4), child: Text(letter, style: const TextStyle(fontSize: 12, color: kDeskMute, fontWeight: FontWeight.w600)), ), for (final r in groups[letter]!) _personTile(r), ], ], ), ), ], ), ), ), const VerticalDivider(width: 1, color: kDeskLine), Expanded( child: selected == null ? const Center(child: Text('选择联系人查看详情', style: TextStyle(color: kDeskMute))) : _detail(selected), ), ], ), ); } Widget _entry(IconData icon, String title, String sub, VoidCallback onTap) { return Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: Row( children: [ DeskAvatar(label: title, api: widget.api, size: 40, color: kDeskBind, icon: icon), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), Text(sub, style: const TextStyle(fontSize: 12, color: kDeskMute)), ], ), ), ], ), ), ), ); } Widget _personTile(Map r) { final id = '${r['id']}'; final online = (_presence[id] ?? const []).contains('online'); return Material( color: _selectedId == id ? kDeskActive : Colors.transparent, child: InkWell( onTap: () => setState(() => _selectedId = id), child: Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( children: [ DeskAvatar(label: '${r['name']}', fileId: '${r['avatarFileId'] ?? ''}', api: widget.api, size: 36), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${r['name']}', style: const TextStyle(fontSize: 14)), Text( [r['department'], r['title']].where((e) => e != null && '$e'.isNotEmpty).join(' · '), style: const TextStyle(fontSize: 12, color: kDeskMute), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Container( width: 8, height: 8, decoration: BoxDecoration( color: online ? kDeskGreen : const Color(0xFFD0D0D0), shape: BoxShape.circle, ), ), ], ), ), ), ); } Widget _detail(Map r) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ DeskAvatar(label: '${r['name']}', fileId: '${r['avatarFileId'] ?? ''}', api: widget.api, size: 80), const SizedBox(height: 16), Text('${r['name']}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), const SizedBox(height: 6), Text( [r['department'], r['title']].where((e) => e != null && '$e'.isNotEmpty).join(' · '), style: const TextStyle(color: kDeskMute), ), const SizedBox(height: 24), _infoRow('手机', '${r['mobile'] ?? '未填写'}'), _infoRow('邮箱', '${r['email'] ?? '未填写'}'), const SizedBox(height: 24), SizedBox( width: double.infinity, child: FilledButton( style: FilledButton.styleFrom(backgroundColor: kDeskBind, minimumSize: const Size.fromHeight(40)), onPressed: () => _message(r), child: const Text('发消息'), ), ), ], ), ), ), ); } Widget _infoRow(String k, String v) { return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: kDeskLine)), child: Row( children: [ SizedBox(width: 56, child: Text(k, style: const TextStyle(color: kDeskMute, fontSize: 13))), Expanded(child: Text(v, style: const TextStyle(fontSize: 14))), ], ), ); } }