import 'dart:async'; import 'package:flutter/material.dart'; import '../api/oa_client.dart'; import '../device/device_bridge.dart'; import '../theme.dart'; import '../widgets/tencent_map_thumb.dart'; import '../widgets/wecom.dart'; class GeoPlace { GeoPlace({required this.lat, required this.lng, required this.title, required this.address, this.distance}); final double lat; final double lng; final String title; final String address; final num? distance; Map toMeta() => { 'lat': lat, 'lng': lng, 'title': title, 'address': address, }; } class LocationPickPage extends StatefulWidget { const LocationPickPage({super.key, required this.api}); final OaClient api; @override State createState() => _LocationPickPageState(); } class _LocationPickPageState extends State { double? _lat; double? _lng; GeoPlace? _picked; List _pois = []; bool _loading = true; String _err = ''; String _q = ''; Timer? _searchDebounce; @override void initState() { super.initState(); _locate(); } @override void dispose() { _searchDebounce?.cancel(); super.dispose(); } Future _locate() async { setState(() { _loading = true; _err = ''; }); try { Map? loc; try { loc = await DeviceBridge.getLocation(); } catch (_) {} final lat = loc?['lat']; final lng = loc?['lng']; if (lat == null || lng == null) { throw Exception('没有拿到定位,请允许位置权限并打开系统定位'); } await _loadAround(lat, lng); } catch (e) { if (mounted) setState(() { _loading = false; _err = '$e'; }); } } Future _loadAround(double lat, double lng) async { final data = await widget.api.post('/office/geo/reverse', {'lat': lat, 'lng': lng}); if (!mounted) return; final map = data is Map ? Map.from(data) : {}; final here = GeoPlace( lat: lat, lng: lng, title: '${map['title'] ?? '当前位置'}', address: '${map['address'] ?? ''}', ); final pois = [here]; final raw = map['pois']; if (raw is List) { for (final e in raw) { if (e is! Map) continue; final plat = (e['lat'] as num?)?.toDouble(); final plng = (e['lng'] as num?)?.toDouble(); if (plat == null || plng == null) continue; pois.add(GeoPlace( lat: plat, lng: plng, title: '${e['title'] ?? e['address'] ?? '地点'}', address: '${e['address'] ?? ''}', distance: e['distance'] as num?, )); } } setState(() { _lat = lat; _lng = lng; _picked = here; _pois = pois; _loading = false; _err = ''; }); } void _onSearch(String q) { _q = q.trim(); _searchDebounce?.cancel(); if (_q.isEmpty) { if (_lat != null && _lng != null) _loadAround(_lat!, _lng!); return; } _searchDebounce = Timer(const Duration(milliseconds: 350), () async { try { final data = await widget.api.get('/office/geo/suggest', query: { 'keyword': _q, if (_lat != null) 'lat': '$_lat', if (_lng != null) 'lng': '$_lng', }); if (!mounted) return; final list = data is List ? data : (data is Map ? (data['items'] ?? data['data'] ?? []) : []); final pois = []; for (final e in (list is List ? list : [])) { if (e is! Map) continue; final plat = (e['lat'] as num?)?.toDouble(); final plng = (e['lng'] as num?)?.toDouble(); if (plat == null || plng == null) continue; pois.add(GeoPlace( lat: plat, lng: plng, title: '${e['title'] ?? '地点'}', address: '${e['address'] ?? ''}', distance: e['distance'] as num?, )); } setState(() { _pois = pois; if (pois.isNotEmpty) _picked = pois.first; }); } catch (_) {} }); } String _dist(num? d) { if (d == null) return ''; if (d < 1) return '当前位置'; if (d < 1000) return '${d.round()}m'; return '${(d / 1000).toStringAsFixed(1)}km'; } @override Widget build(BuildContext context) { final picked = _picked; return Scaffold( backgroundColor: Colors.white, appBar: AppBar( leading: TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')), title: const Text('发送位置'), actions: [ Padding( padding: const EdgeInsets.only(right: 8, top: 8, bottom: 8), child: FilledButton( style: FilledButton.styleFrom(backgroundColor: kWeGreen, minimumSize: const Size(64, 32)), onPressed: picked == null ? null : () => Navigator.pop(context, picked), child: const Text('发送'), ), ), ], ), body: Column( children: [ SizedBox( height: 220, width: double.infinity, child: Stack( children: [ if (picked != null) Positioned.fill( child: TencentMapThumb(api: widget.api, lat: picked.lat, lng: picked.lng, height: 220), ) else const ColoredBox(color: Color(0xFFE8F5E9), child: Center(child: CircularProgressIndicator())), Positioned( left: 12, bottom: 12, child: Material( color: Colors.white, shape: const CircleBorder(), elevation: 2, child: IconButton( onPressed: _locate, icon: const Icon(Icons.my_location, color: kBind), ), ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), child: WxSearchBar(hint: '搜索地点', onChanged: _onSearch), ), if (_err.isNotEmpty) Padding( padding: const EdgeInsets.all(12), child: Text(_err, style: const TextStyle(color: kDanger, fontSize: 13)), ), Expanded( child: _loading ? const Center(child: CircularProgressIndicator()) : ListView.builder( itemCount: _pois.length, itemBuilder: (_, i) { final p = _pois[i]; final on = picked?.lat == p.lat && picked?.lng == p.lng && picked?.title == p.title; return InkWell( onTap: () => setState(() => _picked = p), child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Row( children: [ Expanded( child: Container( padding: const EdgeInsets.only(bottom: 12), decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: kLine, width: 0.5))), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(p.title, style: const TextStyle(fontSize: 16, color: kInk)), const SizedBox(height: 2), Text( [_dist(p.distance), p.address].where((e) => e.isNotEmpty).join(' | '), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: kMute), ), ], ), ), ), if (on) const Padding(padding: EdgeInsets.only(left: 8), child: Icon(Icons.check, color: kWeGreen)), ], ), ), ); }, ), ), ], ), ); } }