You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
13 KiB
478 lines
13 KiB
/** |
|
* 地图找店页面 |
|
*/ |
|
import { View, Text, Map, ScrollView, Image, Input } from '@tarojs/components' |
|
import Taro, { useLoad } from '@tarojs/taro' |
|
import { observer } from 'mobx-react' |
|
import { useState, useRef, useEffect } from 'react' |
|
import { Search, Arrow, Location } from '@taroify/icons' |
|
import './index.scss' |
|
|
|
// 模拟门店数据(带经纬度) |
|
const mockStoreData = [ |
|
{ |
|
id: '1', |
|
name: '北京黑八大师联盟球厅店', |
|
address: '北京市XXX区XXX街道XXX路19号606', |
|
latitude: 39.910823, |
|
longitude: 116.400470, |
|
price: 16.00, |
|
billiardCount: 6, |
|
billiardEmpty: 6, |
|
chessCount: 2, |
|
chessEmpty: 2, |
|
type: '直营店', |
|
phone: '010-12345678', |
|
distance: 0, |
|
distanceText: '0.05km' |
|
}, |
|
{ |
|
id: '2', |
|
name: '北京晶彩桌球世界店', |
|
address: '北京市XXX区XXX街道XXX路XXXXXX大厦101', |
|
latitude: 39.905823, |
|
longitude: 116.395470, |
|
price: 24.00, |
|
billiardCount: 8, |
|
billiardEmpty: 8, |
|
chessCount: 1, |
|
chessEmpty: 1, |
|
type: '加盟店', |
|
phone: '010-87654321', |
|
distance: 0, |
|
distanceText: '0.1km' |
|
}, |
|
{ |
|
id: '3', |
|
name: '北京飓风台球店', |
|
address: '北京市XXX区XXX街道106号A01', |
|
latitude: 39.915823, |
|
longitude: 116.405470, |
|
price: 106.00, |
|
billiardCount: 10, |
|
billiardEmpty: 10, |
|
chessCount: 0, |
|
chessEmpty: 0, |
|
type: '加盟店', |
|
phone: '010-11223344', |
|
distance: 0, |
|
distanceText: '1.3km' |
|
}, |
|
{ |
|
id: '4', |
|
name: '北京星耀台球俱乐部', |
|
address: '北京市XXX区XXX街道XXX路99号', |
|
latitude: 39.900823, |
|
longitude: 116.390470, |
|
price: 30.00, |
|
billiardCount: 12, |
|
billiardEmpty: 10, |
|
chessCount: 3, |
|
chessEmpty: 2, |
|
type: '直营店', |
|
phone: '010-99887766', |
|
distance: 0, |
|
distanceText: '2.3km' |
|
} |
|
] |
|
|
|
interface Store { |
|
id: string |
|
name: string |
|
address: string |
|
latitude: number |
|
longitude: number |
|
price: number |
|
billiardCount: number |
|
billiardEmpty: number |
|
chessCount: number |
|
chessEmpty: number |
|
type: string |
|
phone: string |
|
distance: number |
|
distanceText: string |
|
} |
|
|
|
interface Marker { |
|
id: number |
|
latitude: number |
|
longitude: number |
|
iconPath: string |
|
width: number |
|
height: number |
|
callout?: any |
|
label?: any |
|
} |
|
|
|
const MapStorePage = observer(() => { |
|
// 用户位置(默认北京天安门) |
|
const [userLocation, setUserLocation] = useState({ |
|
latitude: 39.908823, |
|
longitude: 116.397470 |
|
}) |
|
|
|
// 地图中心点(默认跟随用户位置) |
|
const [mapCenter, setMapCenter] = useState({ |
|
latitude: 39.908823, |
|
longitude: 116.397470 |
|
}) |
|
|
|
const [storeList, setStoreList] = useState<Store[]>([]) |
|
const [markers, setMarkers] = useState<Marker[]>([]) |
|
const [activeStoreId, setActiveStoreId] = useState<string | null>(null) |
|
const [searchValue, setSearchValue] = useState('') |
|
const [selectedCity, setSelectedCity] = useState('选城市') |
|
|
|
const mapContext = useRef<any>(null) |
|
|
|
useLoad(() => { |
|
// 页面加载时获取用户位置 |
|
requestLocationPermission() |
|
}) |
|
|
|
useEffect(() => { |
|
// 初始化地图上下文 |
|
mapContext.current = Taro.createMapContext('storeMap') |
|
}, []) |
|
|
|
// 请求位置权限并获取位置 |
|
const requestLocationPermission = async () => { |
|
try { |
|
// 检查权限 |
|
const setting = await Taro.getSetting() |
|
|
|
if (!setting.authSetting['scope.userLocation']) { |
|
// 请求授权 |
|
await Taro.authorize({ scope: 'scope.userLocation' }) |
|
} |
|
|
|
// 获取位置 |
|
Taro.showLoading({ title: '定位中...' }) |
|
|
|
const location = await Taro.getLocation({ |
|
type: 'gcj02', // 返回国测局坐标,适用于微信小程序地图 |
|
isHighAccuracy: true, |
|
altitude: false |
|
}) |
|
|
|
Taro.hideLoading() |
|
|
|
const userPos = { |
|
latitude: location.latitude, |
|
longitude: location.longitude |
|
} |
|
|
|
setUserLocation(userPos) |
|
setMapCenter(userPos) |
|
|
|
// 获取附近门店(使用模拟数据) |
|
fetchNearbyStores(userPos.latitude, userPos.longitude) |
|
|
|
} catch (error) { |
|
Taro.hideLoading() |
|
console.error('获取位置失败:', error) |
|
|
|
// 使用默认位置并加载门店 |
|
fetchNearbyStores(userLocation.latitude, userLocation.longitude) |
|
|
|
Taro.showModal({ |
|
title: '定位提示', |
|
content: '未能获取您的位置,将使用默认位置展示门店', |
|
showCancel: false |
|
}) |
|
} |
|
} |
|
|
|
// 获取附近门店(模拟接口) |
|
const fetchNearbyStores = async (lat: number, lng: number) => { |
|
try { |
|
Taro.showLoading({ title: '加载中...' }) |
|
|
|
// TODO: 替换为真实接口 |
|
// const res = await Taro.request({ |
|
// url: 'https://your-api.com/api/stores/nearby', |
|
// method: 'GET', |
|
// data: { |
|
// latitude: lat, |
|
// longitude: lng, |
|
// radius: 10000, |
|
// limit: 50 |
|
// } |
|
// }) |
|
|
|
// 模拟网络延迟 |
|
await new Promise(resolve => setTimeout(resolve, 500)) |
|
|
|
// 使用模拟数据并计算距离 |
|
const storesWithDistance = mockStoreData.map(store => ({ |
|
...store, |
|
distance: calculateDistance(lat, lng, store.latitude, store.longitude), |
|
distanceText: formatDistance( |
|
calculateDistance(lat, lng, store.latitude, store.longitude) |
|
) |
|
})) |
|
|
|
// 按距离排序 |
|
storesWithDistance.sort((a, b) => a.distance - b.distance) |
|
|
|
setStoreList(storesWithDistance) |
|
|
|
// 生成地图标记 |
|
generateMarkers(storesWithDistance) |
|
|
|
Taro.hideLoading() |
|
|
|
} catch (error) { |
|
Taro.hideLoading() |
|
console.error('获取门店失败:', error) |
|
Taro.showToast({ |
|
title: '获取门店失败', |
|
icon: 'none' |
|
}) |
|
} |
|
} |
|
|
|
// 计算两点之间的距离(米)- Haversine 公式 |
|
const calculateDistance = ( |
|
lat1: number, |
|
lng1: number, |
|
lat2: number, |
|
lng2: number |
|
): number => { |
|
const R = 6371e3 // 地球半径(米) |
|
const φ1 = (lat1 * Math.PI) / 180 |
|
const φ2 = (lat2 * Math.PI) / 180 |
|
const Δφ = ((lat2 - lat1) * Math.PI) / 180 |
|
const Δλ = ((lng2 - lng1) * Math.PI) / 180 |
|
|
|
const a = |
|
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + |
|
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2) |
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) |
|
|
|
return R * c // 返回米 |
|
} |
|
|
|
// 格式化距离显示 |
|
const formatDistance = (distance: number): string => { |
|
if (distance < 1000) { |
|
return `${distance.toFixed(0)}m` |
|
} else { |
|
return `${(distance / 1000).toFixed(2)}km` |
|
} |
|
} |
|
|
|
// 生成地图标记 |
|
const generateMarkers = (stores: Store[]) => { |
|
// 使用默认标记图标(微信小程序会提供默认图标) |
|
// 如果需要自定义图标,准备好图片后替换 iconPath |
|
const newMarkers: Marker[] = stores.map((store, index) => ({ |
|
id: index, |
|
latitude: store.latitude, |
|
longitude: store.longitude, |
|
iconPath: '', // 空字符串使用默认图标,或指定自定义图标路径 |
|
width: 30, |
|
height: 30, |
|
callout: { |
|
content: store.name, |
|
color: '#333333', |
|
fontSize: 12, |
|
borderRadius: 8, |
|
bgColor: '#FFFFFF', |
|
padding: 8, |
|
display: 'BYCLICK' // 点击标记时显示 |
|
} |
|
})) |
|
|
|
setMarkers(newMarkers) |
|
} |
|
|
|
// 点击地图标记 |
|
const handleMarkerTap = (e: any) => { |
|
const markerIndex = e.detail.markerId |
|
const store = storeList[markerIndex] |
|
|
|
if (store) { |
|
setActiveStoreId(store.id) |
|
setMapCenter({ |
|
latitude: store.latitude, |
|
longitude: store.longitude |
|
}) |
|
} |
|
} |
|
|
|
// 点击列表项 |
|
const handleStoreItemClick = (store: Store) => { |
|
setActiveStoreId(store.id) |
|
|
|
// 移动地图到对应位置 |
|
setMapCenter({ |
|
latitude: store.latitude, |
|
longitude: store.longitude |
|
}) |
|
|
|
// 使用 moveToLocation 移动地图(需要 mapContext) |
|
if (mapContext.current) { |
|
mapContext.current.moveToLocation({ |
|
latitude: store.latitude, |
|
longitude: store.longitude |
|
}) |
|
} |
|
} |
|
|
|
// 重新定位 |
|
const handleRelocate = () => { |
|
requestLocationPermission() |
|
} |
|
|
|
// 导航到门店 |
|
const handleNavigate = (store: Store, e: any) => { |
|
e.stopPropagation() |
|
|
|
Taro.openLocation({ |
|
latitude: store.latitude, |
|
longitude: store.longitude, |
|
name: store.name, |
|
address: store.address, |
|
scale: 18 |
|
}) |
|
} |
|
|
|
// 搜索门店 |
|
const handleSearch = () => { |
|
if (!searchValue.trim()) { |
|
Taro.showToast({ |
|
title: '请输入门店名称或地址', |
|
icon: 'none' |
|
}) |
|
return |
|
} |
|
|
|
// TODO: 调用搜索接口 |
|
const filtered = mockStoreData.filter( |
|
store => |
|
store.name.includes(searchValue) || store.address.includes(searchValue) |
|
) |
|
|
|
if (filtered.length > 0) { |
|
setStoreList(filtered) |
|
generateMarkers(filtered) |
|
|
|
// 移动到第一个搜索结果 |
|
setMapCenter({ |
|
latitude: filtered[0].latitude, |
|
longitude: filtered[0].longitude |
|
}) |
|
} else { |
|
Taro.showToast({ |
|
title: '未找到相关门店', |
|
icon: 'none' |
|
}) |
|
} |
|
} |
|
|
|
return ( |
|
<View className="map-store-page"> |
|
{/* 地图区域 */} |
|
<Map |
|
id="storeMap" |
|
className="map-container" |
|
longitude={mapCenter.longitude} |
|
latitude={mapCenter.latitude} |
|
scale={15} |
|
markers={markers} |
|
show-location |
|
enable-zoom |
|
enable-scroll |
|
enable-rotate={false} |
|
onMarkerTap={handleMarkerTap} |
|
onError={() => {}} |
|
/> |
|
|
|
{/* 重新定位按钮 */} |
|
<View className="relocate-btn" onClick={handleRelocate}> |
|
<Location size={24} color="#333" /> |
|
</View> |
|
|
|
{/* 底部内容区域 */} |
|
<View className="content-area"> |
|
{/* 筛选区域 */} |
|
<View className="filter-area"> |
|
<View className="filter-left"> |
|
{/* 城市选择 */} |
|
<View className="city-select"> |
|
<Text className="city-text">{selectedCity}</Text> |
|
<Arrow size={16} color="#999999" /> |
|
</View> |
|
|
|
{/* 搜索输入框 */} |
|
<View className="search-input-wrapper"> |
|
<Input |
|
className="search-input" |
|
placeholder="搜索门店名称/地址" |
|
placeholderClass="search-placeholder" |
|
value={searchValue} |
|
onInput={(e) => setSearchValue(e.detail.value)} |
|
/> |
|
<View className="search-icon-btn" onClick={handleSearch}> |
|
<Search size={18} color="#999999" /> |
|
</View> |
|
</View> |
|
</View> |
|
</View> |
|
|
|
{/* 门店列表 */} |
|
<View className="list-header"> |
|
<Text className="list-title">附近门店</Text> |
|
<Text className="list-count">共 {storeList.length} 家</Text> |
|
</View> |
|
|
|
<ScrollView scrollY className="store-list"> |
|
{storeList.map((store) => ( |
|
<View |
|
key={store.id} |
|
className={`store-item ${activeStoreId === store.id ? 'active' : ''}`} |
|
onClick={() => handleStoreItemClick(store)} |
|
> |
|
{/* 头部:名称和距离 */} |
|
<View className="store-header"> |
|
<Text className="store-name">{store.name}</Text> |
|
<Text className="store-distance">距离{store.distanceText}</Text> |
|
</View> |
|
|
|
{/* 地址 */} |
|
<View className="store-address"> |
|
<Text className="address-text">{store.address}</Text> |
|
</View> |
|
|
|
{/* 价格信息 */} |
|
<View className="store-price"> |
|
<View className="price-item"> |
|
<Text className="price-label">台球</Text> |
|
<Text className="price-symbol">¥</Text> |
|
<Text className="price-value">{store.price.toFixed(2)}</Text> |
|
<Text className="price-unit">起</Text> |
|
</View> |
|
<View className="price-item"> |
|
<Text className="price-label">棋牌</Text> |
|
<Text className="price-symbol">¥</Text> |
|
<Text className="price-value">{(store.price + 4).toFixed(2)}</Text> |
|
<Text className="price-unit">起</Text> |
|
</View> |
|
</View> |
|
</View> |
|
))} |
|
|
|
{storeList.length === 0 && ( |
|
<View className="empty-state"> |
|
<Text className="empty-text">暂无门店数据</Text> |
|
</View> |
|
)} |
|
</ScrollView> |
|
</View> |
|
</View> |
|
) |
|
}) |
|
|
|
export default MapStorePage
|
|
|