import { useMemo, useRef, useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import {Input, Text, View} from '@tarojs/components' import { Empty, InfiniteLoading, Loading, PullToRefresh } from '@nutui/nutui-react-taro' import {Search} from '@nutui/icons-react-taro' import { pageShopStore } from '@/api/shop/shopStore' import type { ShopStore } from '@/api/shop/shopStore/model' import { getCurrentLngLat } from '@/utils/location' import './find.scss' const PAGE_SIZE = 10 type LngLat = { lng: number; lat: number } type ShopStoreView = ShopStore & { __distanceMeter?: number } const parseLngLat = (raw: string | undefined): LngLat | null => { const text = (raw || '').trim() if (!text) return null const parts = text.split(/[,\s]+/).filter(Boolean) if (parts.length < 2) return null const a = Number(parts[0]) const b = Number(parts[1]) if (!Number.isFinite(a) || !Number.isFinite(b)) return null // Accept both "lng,lat" and "lat,lng". const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90 const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180 if (looksLikeLngLat) return { lng: a, lat: b } if (looksLikeLatLng) return { lng: b, lat: a } return null } const distanceMeters = (a: LngLat, b: LngLat) => { const toRad = (x: number) => (x * Math.PI) / 180 const R = 6371000 const dLat = toRad(b.lat - a.lat) const dLng = toRad(b.lng - a.lng) const lat1 = toRad(a.lat) const lat2 = toRad(b.lat) const sin1 = Math.sin(dLat / 2) const sin2 = Math.sin(dLng / 2) const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2 return 2 * R * Math.asin(Math.min(1, Math.sqrt(h))) } const formatDistance = (meter: number | undefined) => { if (!Number.isFinite(meter as number)) return '' const m = Math.max(0, Math.round(meter as number)) if (m < 1000) return `${m}米` const km = m / 1000 return `${km.toFixed(km >= 10 ? 0 : 1)}km` } const Find = () => { const [keyword, setKeyword] = useState('') const [storeList, setStoreList] = useState([]) const [loading, setLoading] = useState(false) const [hasMore, setHasMore] = useState(true) const [userLngLat, setUserLngLat] = useState(null) const pageRef = useRef(1) const latestListRef = useRef([]) const loadingRef = useRef(false) const coordsRef = useRef(null) const viewList = useMemo(() => { const me = userLngLat if (!me) return storeList // Keep backend order; only attach distance for display. return storeList.map((s) => { const coords = parseLngLat(s.lngAndLat || s.location) if (!coords) return s return { ...s, __distanceMeter: distanceMeters(me, coords) } }) }, [storeList, userLngLat]) const loadStores = async (isRefresh = true, keywordsOverride?: string) => { if (loadingRef.current) return loadingRef.current = true setLoading(true) if (isRefresh) { pageRef.current = 1 latestListRef.current = [] setStoreList([]) setHasMore(true) } try { if (!coordsRef.current) { const me = await getCurrentLngLat('为您展示附近网点,需要获取定位信息。') const lng = me ? Number(me.lng) : NaN const lat = me ? Number(me.lat) : NaN coordsRef.current = Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : null setUserLngLat(coordsRef.current) } const currentPage = pageRef.current const kw = (keywordsOverride ?? keyword).trim() const res = await pageShopStore({ page: currentPage, limit: PAGE_SIZE, keywords: kw || undefined }) const resList = res?.list || [] const nextList = isRefresh ? resList : [...latestListRef.current, ...resList] latestListRef.current = nextList setStoreList(nextList) const count = typeof res?.count === 'number' ? res.count : nextList.length setHasMore(nextList.length < count) if (resList.length > 0) { pageRef.current = currentPage + 1 } else { setHasMore(false) } } catch (e) { console.error('获取网点列表失败:', e) Taro.showToast({ title: '获取网点失败', icon: 'none' }) setHasMore(false) } finally { loadingRef.current = false setLoading(false) } } useDidShow(() => { loadStores(true).then() }) const onNavigate = (item: ShopStore) => { const coords = parseLngLat(item.lngAndLat || item.location) if (!coords) { Taro.showToast({ title: '网点暂无坐标,无法导航', icon: 'none' }) return } Taro.openLocation({ latitude: coords.lat, longitude: coords.lng, name: item.name || item.city || '网点', address: item.address || '' }) } const onCall = (phone: string | undefined) => { const p = (phone || '').trim() if (!p) { Taro.showToast({ title: '暂无联系电话', icon: 'none' }) return } Taro.makePhoneCall({ phoneNumber: p }) } const onSearch = () => { loadStores(true).then() } return ( setKeyword(e.detail.value)} onConfirm={onSearch} /> loadStores(true)} headHeight={60}> {viewList.length === 0 && !loading ? ( ) : ( loadStores(false)} loadingText={ 加载中... } loadMoreText={ {viewList.length === 0 ? '暂无网点' : '没有更多了'} } > {viewList.map((item, idx) => { const name = item?.name || item?.city || item?.province || '网点' const contact = item?.managerName || '--' const distanceText = formatDistance(item?.__distanceMeter) return ( 网点名称: {name} 网点地址: {item?.address || '--'} 联系电话: onCall(item?.phone)}> {item?.phone || '--'} 联系人: {contact} onNavigate(item)}> {distanceText ? `${distanceText}` : '查看导航'} ) })} )} ) } export default Find