台球开关台小程序
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

/**
* 地图找店页面
*/
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