14 changed files with 1351 additions and 26 deletions
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,5 @@ |
|||||||
|
export default { |
||||||
|
navigationBarTitleText: '地图找店', |
||||||
|
navigationBarBackgroundColor: '#FFFFFF', |
||||||
|
navigationBarTextStyle: 'black' |
||||||
|
} |
||||||
@ -0,0 +1,221 @@ |
|||||||
|
.map-store-page { |
||||||
|
width: 100%; |
||||||
|
height: 100vh; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
background: #F5F5F5; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
// 地图容器 |
||||||
|
.map-container { |
||||||
|
width: 100%; |
||||||
|
height: 650px; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
// 重新定位按钮 |
||||||
|
.relocate-btn { |
||||||
|
position: absolute; |
||||||
|
top: 550px; |
||||||
|
right: 30px; |
||||||
|
width: 80px; |
||||||
|
height: 80px; |
||||||
|
background: #FFFFFF; |
||||||
|
border-radius: 50%; |
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
z-index: 10; |
||||||
|
} |
||||||
|
|
||||||
|
// 底部内容区域 |
||||||
|
.content-area { |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
background: #F2F3F5; |
||||||
|
border-radius: 30px 30px 0 0; |
||||||
|
margin-top: -30px; |
||||||
|
padding: 30px 30px 0; |
||||||
|
position: relative; |
||||||
|
z-index: 5; |
||||||
|
box-shadow: 0 -3px 14px 0 rgba(202, 203, 204, 0.5); |
||||||
|
|
||||||
|
// 筛选区域 |
||||||
|
.filter-area { |
||||||
|
// margin-bottom: 30px; |
||||||
|
|
||||||
|
.filter-left { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 20px; |
||||||
|
|
||||||
|
// 城市选择 |
||||||
|
.city-select { |
||||||
|
flex-shrink: 0; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 8px; |
||||||
|
padding: 16px 24px; |
||||||
|
background: #FFFFFF; |
||||||
|
border-radius: 8px; |
||||||
|
|
||||||
|
.city-text { |
||||||
|
font-weight: 400; |
||||||
|
font-size: 29px; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 搜索输入框 |
||||||
|
.search-input-wrapper { |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
background: #FFFFFF; |
||||||
|
border-radius: 8px; |
||||||
|
padding: 16px 24px; |
||||||
|
gap: 16px; |
||||||
|
|
||||||
|
.search-input { |
||||||
|
flex: 1; |
||||||
|
font-size: 29px; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
|
||||||
|
.search-placeholder { |
||||||
|
font-size: 29px; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
|
||||||
|
.search-icon-btn { |
||||||
|
flex-shrink: 0; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 列表头部 |
||||||
|
.list-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
margin: 30px 0; |
||||||
|
|
||||||
|
.list-title { |
||||||
|
font-size: 29px; |
||||||
|
font-weight: 400; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
|
||||||
|
.list-count { |
||||||
|
font-size: 26px; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 门店列表 |
||||||
|
.store-list { |
||||||
|
flex: 1; |
||||||
|
margin: 0 -30px; |
||||||
|
padding: 0 30px; |
||||||
|
|
||||||
|
.store-item { |
||||||
|
padding: 30px; |
||||||
|
background: #FFFFFF; |
||||||
|
border-radius: 13px; |
||||||
|
margin-bottom: 28px; |
||||||
|
transition: all 0.3s; |
||||||
|
|
||||||
|
&.active { |
||||||
|
background: #FFF7E6; |
||||||
|
border: 2px solid #FF9900; |
||||||
|
box-shadow: 0 4px 12px rgba(255, 153, 0, 0.2); |
||||||
|
} |
||||||
|
|
||||||
|
// 头部:名称和距离 |
||||||
|
.store-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
margin-bottom: 22px; |
||||||
|
|
||||||
|
.store-name { |
||||||
|
flex: 1; |
||||||
|
font-weight: 500; |
||||||
|
font-size: 33px; |
||||||
|
color: #000000; |
||||||
|
margin-right: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.store-distance { |
||||||
|
flex-shrink: 0; |
||||||
|
font-size: 25px; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 地址 |
||||||
|
.store-address { |
||||||
|
margin-bottom: 22px; |
||||||
|
|
||||||
|
.address-text { |
||||||
|
font-size: 25px; |
||||||
|
color: #595959; |
||||||
|
line-height: 1.5; |
||||||
|
word-break: break-all; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 价格信息 |
||||||
|
.store-price { |
||||||
|
display: flex; |
||||||
|
align-items: baseline; |
||||||
|
gap: 30px; |
||||||
|
|
||||||
|
.price-item { |
||||||
|
display: flex; |
||||||
|
align-items: baseline; |
||||||
|
|
||||||
|
.price-label { |
||||||
|
font-size: 25px; |
||||||
|
color: #8B8B8B; |
||||||
|
margin-right: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
.price-symbol { |
||||||
|
font-size: 25px; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
|
||||||
|
.price-value { |
||||||
|
font-size: 25px; |
||||||
|
font-weight: 500; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
|
||||||
|
.price-unit { |
||||||
|
font-size: 25px; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 空状态 |
||||||
|
.empty-state { |
||||||
|
padding: 100px 0; |
||||||
|
text-align: center; |
||||||
|
|
||||||
|
.empty-text { |
||||||
|
font-size: 28px; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,478 @@ |
|||||||
|
/** |
||||||
|
* 地图找店页面 |
||||||
|
*/ |
||||||
|
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 |
||||||
@ -1,19 +1,299 @@ |
|||||||
|
/** |
||||||
|
* 门店列表页样式 |
||||||
|
* 设计稿宽度: 750px |
||||||
|
*/ |
||||||
|
|
||||||
.store-page { |
.store-page { |
||||||
min-height: 100vh; |
// 高度为 100vh 减去 tabbar 高度 |
||||||
|
height: calc(100vh - 40Px); |
||||||
|
height: calc(100vh - 40Px - constant(safe-area-inset-bottom)); |
||||||
|
height: calc(100vh - 40Px - env(safe-area-inset-bottom)); |
||||||
background-color: #121212; |
background-color: #121212; |
||||||
|
position: relative; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: auto; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
/* 内容区域 */ |
||||||
|
.content-area { |
||||||
|
padding-top: 175px; |
||||||
|
padding-left: 30px; |
||||||
|
padding-right: 30px; |
||||||
|
} |
||||||
|
|
||||||
.header { |
/* 筛选区域 */ |
||||||
padding: 32px; |
.filter-area { |
||||||
background: #fff; |
height: 84px; |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
.title { |
.filter-left { |
||||||
font-size: 36px; |
flex: 1; |
||||||
font-weight: bold; |
display: flex; |
||||||
color: #333; |
align-items: center; |
||||||
} |
background-color: #21212A; |
||||||
|
border-radius: 10px; |
||||||
|
height: 100%; |
||||||
|
padding: 0 20px; |
||||||
|
} |
||||||
|
|
||||||
|
/* 城市选择 */ |
||||||
|
.city-select { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
flex-shrink: 0; |
||||||
|
padding-right: 20px; |
||||||
|
border-right: 1px solid #333; |
||||||
|
margin-right: 20px; |
||||||
|
|
||||||
|
.city-text { |
||||||
|
font-weight: 400; |
||||||
|
font-size: 29px; |
||||||
|
color: #FFFFFF; |
||||||
|
margin-right: 8px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 搜索输入框包装器 */ |
||||||
|
.search-input-wrapper { |
||||||
|
flex: 1; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
height: 100%; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
/* 搜索输入框 */ |
||||||
|
.search-input { |
||||||
|
flex: 1; |
||||||
|
height: 100%; |
||||||
|
font-size: 29px; |
||||||
|
color: #FFFFFF; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.search-placeholder { |
||||||
|
font-size: 29px; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
|
||||||
|
/* 输入框右侧图标按钮(搜索/清除) */ |
||||||
|
.input-icon-btn { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
width: 50px; |
||||||
|
height: 50px; |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* 找店按钮 */ |
||||||
|
.find-store-btn { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
margin-left: 30px; |
||||||
|
flex-shrink: 0; |
||||||
|
|
||||||
|
.store-icon { |
||||||
|
width: 50px; |
||||||
|
height: 50px; |
||||||
|
margin-bottom: 6px; |
||||||
} |
} |
||||||
|
|
||||||
.content { |
.find-text { |
||||||
padding: 32px; |
font-weight: 400; |
||||||
|
font-size: 21px; |
||||||
|
color: #E5E5E5; |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
|
/* 门店列表 */ |
||||||
|
.store-list { |
||||||
|
margin-top: 30px; |
||||||
|
} |
||||||
|
|
||||||
|
/* 门店项 */ |
||||||
|
.store-item { |
||||||
|
background-color: #21212A; |
||||||
|
border-radius: 13px; |
||||||
|
padding: 28px; |
||||||
|
margin-bottom: 28px; |
||||||
|
} |
||||||
|
|
||||||
|
/* 门店头部 - 名称和价格 */ |
||||||
|
.store-header { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: flex-start; |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.store-name { |
||||||
|
font-weight: 500; |
||||||
|
font-size: 33px; |
||||||
|
color: #FFFFFF; |
||||||
|
flex: 1; |
||||||
|
margin-right: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.store-price { |
||||||
|
display: flex; |
||||||
|
align-items: baseline; |
||||||
|
flex-shrink: 0; |
||||||
|
|
||||||
|
.price-symbol { |
||||||
|
font-size: 25px; |
||||||
|
color: #01CA68; |
||||||
|
} |
||||||
|
|
||||||
|
.price-value { |
||||||
|
font-size: 33px; |
||||||
|
color: #01CA68; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.price-unit { |
||||||
|
font-size: 25px; |
||||||
|
color: #01CA68; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 标签区域 */ |
||||||
|
.store-tags { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
gap: 16px; |
||||||
|
margin-bottom: 24px; |
||||||
|
} |
||||||
|
|
||||||
|
.tag { |
||||||
|
padding: 8px 16px; |
||||||
|
border-radius: 6px; |
||||||
|
font-size: 23px; |
||||||
|
border-width: 1px; |
||||||
|
border-style: solid; |
||||||
|
} |
||||||
|
|
||||||
|
.tag-billiard { |
||||||
|
color: #01CA68; |
||||||
|
border-color: #01CA68; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.tag-chess { |
||||||
|
color: #FF9900; |
||||||
|
border-color: #FF9900; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.tag-type { |
||||||
|
color: #E5E5E5; |
||||||
|
border-color: #E5E5E5; |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
/* 地址和距离 */ |
||||||
|
.store-location { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
.location-left { |
||||||
|
display: flex; |
||||||
|
align-items: flex-start; |
||||||
|
max-width: 389px; |
||||||
|
} |
||||||
|
|
||||||
|
.location-icon { |
||||||
|
width: 28px; |
||||||
|
height: 28px; |
||||||
|
flex-shrink: 0; |
||||||
|
margin-right: 8px; |
||||||
|
margin-top: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.address-text { |
||||||
|
font-size: 25px; |
||||||
|
color: #E5E5E5; |
||||||
|
line-height: 1.4; |
||||||
|
word-break: break-all; |
||||||
|
} |
||||||
|
|
||||||
|
.distance-text { |
||||||
|
font-size: 25px; |
||||||
|
color: #E5E5E5; |
||||||
|
flex-shrink: 0; |
||||||
|
margin-left: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
/* 列表底部 */ |
||||||
|
.list-footer { |
||||||
|
text-align: center; |
||||||
|
padding: 40px 0; |
||||||
|
|
||||||
|
.footer-text { |
||||||
|
font-size: 26px; |
||||||
|
color: #666; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 城市选择弹窗 */ |
||||||
|
.city-picker { |
||||||
|
background: #fff; |
||||||
|
border-radius: 20px 20px 0 0; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-header { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
padding: 30px; |
||||||
|
border-bottom: 1px solid #eee; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-cancel { |
||||||
|
font-size: 30px; |
||||||
|
color: #666; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-title { |
||||||
|
font-size: 32px; |
||||||
|
font-weight: 500; |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-confirm { |
||||||
|
font-size: 30px; |
||||||
|
color: #01CA68; |
||||||
|
} |
||||||
|
|
||||||
|
/* Cascader 选项列表 */ |
||||||
|
.cascader-options { |
||||||
|
height: 600px; |
||||||
|
background: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.cascader-option { |
||||||
|
padding: 30px; |
||||||
|
border-bottom: 1px solid #f5f5f5; |
||||||
|
|
||||||
|
&.active { |
||||||
|
.option-text { |
||||||
|
color: #01CA68; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.option-text { |
||||||
|
font-size: 28px; |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
|
||||||
|
|||||||
Loading…
Reference in new issue