Browse Source

店铺,找店,导航

master
DU 6 months ago
parent
commit
1c540db321
  1. 5
      babel.config.js
  2. 1
      package-lock.json
  3. 1
      package.json
  4. 17
      src/app.config.ts
  5. 9
      src/app.scss
  6. 2
      src/app.ts
  7. BIN
      src/assets/icon/locationIcon.png
  8. BIN
      src/assets/icon/storeIcon.png
  9. 2
      src/custom-tab-bar/index.scss
  10. 5
      src/pageStore/mapStore/index.config.ts
  11. 221
      src/pageStore/mapStore/index.scss
  12. 478
      src/pageStore/mapStore/index.tsx
  13. 304
      src/pages/store/index.scss
  14. 332
      src/pages/store/index.tsx

5
babel.config.js

@ -10,13 +10,12 @@ module.exports = { @@ -10,13 +10,12 @@ module.exports = {
}]
],
plugins: [
// Taroify 组件按需导入配置
[
"import",
{
libraryName: "@taroify/core",
libraryDirectory: "",
style: false, // 样式已在 app.ts 中全局导入
style: true,
},
"@taroify/core",
],
@ -26,7 +25,7 @@ module.exports = { @@ -26,7 +25,7 @@ module.exports = {
libraryName: "@taroify/icons",
libraryDirectory: "",
camel2DashComponentName: false,
style: false,
style: () => "@taroify/icons/style",
customName: (name) => name === "Icon" ? "@taroify/icons/van/VanIcon" : `@taroify/icons/${name}`,
},
"@taroify/icons",

1
package-lock.json generated

@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
"dependencies": {
"@babel/runtime": "^7.24.4",
"@taroify/core": "^0.9.0",
"@taroify/hooks": "^0.9.0",
"@taroify/icons": "^0.9.0",
"@tarojs/components": "4.0.9",
"@tarojs/helper": "4.0.9",

1
package.json

@ -35,6 +35,7 @@ @@ -35,6 +35,7 @@
"dependencies": {
"@babel/runtime": "^7.24.4",
"@taroify/core": "^0.9.0",
"@taroify/hooks": "^0.9.0",
"@taroify/icons": "^0.9.0",
"@tarojs/components": "4.0.9",
"@tarojs/helper": "4.0.9",

17
src/app.config.ts

@ -7,6 +7,23 @@ export default defineAppConfig({ @@ -7,6 +7,23 @@ export default defineAppConfig({
'pages/user/index',
'pages/scanCode/index', // 独立扫码页面(无tabbar)
],
// 分包配置
subpackages: [
{
root: 'pageStore',
name: 'store-package',
pages: [
'mapStore/index', // 地图找店页面
]
}
],
// 分包预下载规则
preloadRule: {
'pages/store/index': {
network: 'all',
packages: ['store-package']
}
},
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',

9
src/app.scss

@ -64,3 +64,12 @@ page { @@ -64,3 +64,12 @@ page {
color: #999;
font-size: 28px;
}
/* 覆盖 taroify Popup 的 z-index,使其高于自定义 tabbar (z-index: 9999) */
.taroify-popup {
z-index: 10001 !important;
}
.taroify-backdrop {
z-index: 10000 !important;
}

2
src/app.ts

@ -3,8 +3,6 @@ import { useLaunch } from '@tarojs/taro' @@ -3,8 +3,6 @@ import { useLaunch } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { userStore } from './store'
import './app.scss'
// 导入 taroify 全局样式
import '@taroify/core/index.scss'
function App({ children }: PropsWithChildren<any>) {
useLaunch(() => {

BIN
src/assets/icon/locationIcon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
src/assets/icon/storeIcon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

2
src/custom-tab-bar/index.scss

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
right: 0;
width: 100%;
height: 100Px; /* 继续减小高度,给凸起留足空间 */
z-index: 9999;
z-index: 99;
pointer-events: none;
// 背景白色层 #0E0D0D

5
src/pageStore/mapStore/index.config.ts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '地图找店',
navigationBarBackgroundColor: '#FFFFFF',
navigationBarTextStyle: 'black'
}

221
src/pageStore/mapStore/index.scss

@ -0,0 +1,221 @@ @@ -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;
}
}
}
}
}

478
src/pageStore/mapStore/index.tsx

@ -0,0 +1,478 @@ @@ -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

304
src/pages/store/index.scss

@ -1,19 +1,299 @@ @@ -1,19 +1,299 @@
/**
* 门店列表页样式
* 设计稿宽度: 750px
*/
.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;
position: relative;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.header {
padding: 32px;
background: #fff;
.title {
font-size: 36px;
font-weight: bold;
color: #333;
}
/* 内容区域 */
.content-area {
padding-top: 175px;
padding-left: 30px;
padding-right: 30px;
}
/* 筛选区域 */
.filter-area {
height: 84px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.filter-left {
flex: 1;
display: flex;
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;
}
.find-text {
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;
}
.content {
padding: 32px;
.cascader-option {
padding: 30px;
border-bottom: 1px solid #f5f5f5;
&.active {
.option-text {
color: #01CA68;
font-weight: 500;
}
}
}
.option-text {
font-size: 28px;
color: #333;
}

332
src/pages/store/index.tsx

@ -1,26 +1,342 @@ @@ -1,26 +1,342 @@
/**
*
*/
import { View, Text } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { View, Text, Image, Input, ScrollView } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { observer } from 'mobx-react'
import { useState } from 'react'
import { Popup, Toast, Tabs } from '@taroify/core'
import { useCascader } from '@taroify/hooks'
import { Arrow, Cross, Search } from '@taroify/icons'
import storeIcon from '../../assets/icon/storeIcon.png'
import locationIcon from '../../assets/icon/locationIcon.png'
import './index.scss'
// 城市数据 - useCascader 格式(嵌套 children)
const cityOptions = [
{
label: '天津市',
value: 'tianjin',
children: [{ label: '天津市', value: 'tianjin_city' }],
},
{
label: '河北省',
value: 'hebei',
children: [
{ label: '赤峰市', value: 'chifeng' },
{ label: '通辽市', value: 'tongliao' },
],
},
{
label: '山西省',
value: 'shanxi',
children: [{ label: '鄂尔多斯市', value: 'eerduosi' }],
},
{
label: '内蒙古自治区',
value: 'neimenggu',
children: [
{ label: '呼伦贝尔市', value: 'hulunbeier' },
{ label: '巴彦淖尔市', value: 'bayannaoer' },
{ label: '乌兰察布市', value: 'wulanchabu' },
{ label: '锡林郭勒盟', value: 'xilingol' },
],
},
{
label: '辽宁省',
value: 'liaoning',
children: [{ label: '沈阳市', value: 'shenyang' }],
},
{
label: '吉林省',
value: 'jilin',
children: [{ label: '长春市', value: 'changchun' }],
},
{
label: '黑龙江省',
value: 'heilongjiang',
children: [{ label: '哈尔滨市', value: 'haerbin' }],
},
]
// 模拟门店数据
const mockStoreList = [
{
id: '1',
name: '北京黑八大师联盟球厅店',
price: 16.00,
billiardCount: 6,
billiardEmpty: 6,
chessCount: 2,
chessEmpty: 2,
type: '直营店',
address: '北京市XXX区XXX街道XXX路19号606',
distance: '0.05km',
},
{
id: '2',
name: '北京晶彩桌球世界店',
price: 24.00,
billiardCount: 8,
billiardEmpty: 8,
chessCount: 1,
chessEmpty: 1,
type: '加盟店',
address: '北京市XXX区XXX街道XXX路XXXXXX大厦101',
distance: '0.1km',
},
{
id: '3',
name: '北京飓风台球店',
price: 106.00,
billiardCount: 0,
billiardEmpty: 0,
chessCount: 0,
chessEmpty: 0,
type: '加盟店',
address: '北京市XXX区XXX街道106号A01',
distance: '100.00km',
},
]
const StoreIndex = observer(() => {
const [selectedCity, setSelectedCity] = useState('选城市')
const [searchValue, setSearchValue] = useState('')
const [cityPopupOpen, setCityPopupOpen] = useState(false)
const [storeList, setStoreList] = useState(mockStoreList)
const [loading, setLoading] = useState(false)
const [isSearched, setIsSearched] = useState(false) // 是否已执行搜索
const [cityValue, setCityValue] = useState<string[]>([])
const [tabIndex, setTabIndex] = useState(0)
// 使用 useCascader hook
const { columns } = useCascader({
value: cityValue,
depth: 2,
options: cityOptions,
})
useLoad(() => {
console.log('门店页面加载')
})
// 打开城市选择
const handleOpenCityPicker = () => {
setCityPopupOpen(true)
}
// 关闭城市选择
const handleCloseCityPicker = () => {
setCityPopupOpen(false)
}
// 选择选项
const handleSelectOption = (option: any, columnIndex: number) => {
const newValue = [...cityValue]
newValue[columnIndex] = option.value
// 清除后面的选中值
newValue.splice(columnIndex + 1)
setCityValue(newValue)
// 如果选择了最后一级(城市),关闭弹窗
if (columnIndex === 1 || !option.children || option.children.length === 0) {
setSelectedCity(option.label)
setCityPopupOpen(false)
setTabIndex(0)
} else {
// 切换到下一个 tab
setTabIndex(columnIndex + 1)
}
}
// 搜索门店
const handleSearch = () => {
// 无内容时提示输入
if (!searchValue.trim()) {
Toast.open('请输入门店名称或地址')
return
}
setLoading(true)
Toast.loading({
message: '搜索中...',
duration: 0,
})
// 模拟接口请求
setTimeout(() => {
setLoading(false)
Toast.close()
Toast.success('搜索完成')
setIsSearched(true) // 标记已搜索,切换为取消图标
// 根据输入内容过滤数据
setStoreList(mockStoreList.filter(store =>
store.name.includes(searchValue) || store.address.includes(searchValue)
))
}, 1500)
}
// 输入框变化
const handleInputChange = (e: { detail: { value: string } }) => {
setSearchValue(e.detail.value)
}
// 清除搜索并恢复默认数据
const handleClearSearch = () => {
setSearchValue('')
setIsSearched(false) // 切换回搜索图标
// 恢复默认数据
setLoading(true)
Toast.loading({
message: '加载中...',
duration: 0,
})
setTimeout(() => {
setLoading(false)
Toast.close()
setStoreList(mockStoreList)
}, 500)
}
// 跳转到地图找店页面(分包页面)
const handleGoToMap = () => {
Taro.navigateTo({
url: '/pageStore/mapStore/index'
})
}
return (
<View className="store-page">
<View className="header">
<Text className="title"></Text>
</View>
<View className="content">
<View className="placeholder">
<Text>...</Text>
{/* 内容区域 */}
<View className="content-area">
{/* 筛选区域 */}
<View className="filter-area">
<View className="filter-left">
{/* 城市选择 */}
<View className="city-select" onClick={handleOpenCityPicker}>
<Text className="city-text">{selectedCity}</Text>
<Arrow size={16} color="#999" />
</View>
{/* 搜索输入框 */}
<View className="search-input-wrapper">
<Input
className="search-input"
placeholder="搜索门店名称/地址"
placeholderClass="search-placeholder"
value={searchValue}
onInput={handleInputChange}
/>
{/* 已搜索时显示取消图标,未搜索时显示搜索图标 */}
{isSearched ? (
<View className="input-icon-btn" onClick={handleClearSearch}>
<Cross size={18} color="#FFFFFF" />
</View>
) : (
<View className="input-icon-btn" onClick={handleSearch}>
<Search size={18} color="#FFFFFF" />
</View>
)}
</View>
</View>
{/* 找店按钮 */}
<View className="find-store-btn" onClick={handleGoToMap}>
<Image className="store-icon" src={storeIcon} mode="aspectFit" />
<Text className="find-text"></Text>
</View>
</View>
{/* 门店列表 */}
<View className="store-list">
{storeList.map((store) => (
<View className="store-item" key={store.id}>
{/* 标题和价格 */}
<View className="store-header">
<Text className="store-name">{store.name}</Text>
<View className="store-price">
<Text className="price-symbol">¥</Text>
<Text className="price-value">{store.price.toFixed(2)}</Text>
<Text className="price-unit">/</Text>
</View>
</View>
{/* 标签 */}
<View className="store-tags">
<View className="tag tag-billiard">
{store.billiardCount}
</View>
<View className="tag tag-chess">
{store.chessCount}
</View>
<View className="tag tag-type">
{store.type}
</View>
</View>
{/* 地址和距离 */}
<View className="store-location">
<View className="location-left">
<Image className="location-icon" src={locationIcon} mode="aspectFit" />
<Text className="address-text">{store.address}</Text>
</View>
<Text className="distance-text">{store.distance} {'>'}</Text>
</View>
</View>
))}
{/* 列表底部提示 */}
<View className="list-footer">
<Text className="footer-text">~</Text>
</View>
</View>
</View>
{/* 城市选择弹窗 - 使用 useCascader */}
<Popup
open={cityPopupOpen}
placement="bottom"
rounded
onClose={handleCloseCityPicker}
>
<View className="city-picker">
{/* 头部 */}
<View className="picker-header">
<Text className="picker-cancel" onClick={handleCloseCityPicker}></Text>
<Text className="picker-title"></Text>
<Text className="picker-confirm" onClick={handleCloseCityPicker}></Text>
</View>
{/* Tabs 切换 */}
<Tabs value={tabIndex} onChange={setTabIndex}>
{columns.map((column, columnIndex) => (
<Tabs.TabPane
key={columnIndex}
title={cityValue[columnIndex]
? column.find(opt => opt.value === cityValue[columnIndex])?.label || '请选择'
: '请选择'
}
>
<ScrollView scrollY className="cascader-options">
{column.map((option) => (
<View
key={option.value}
className={`cascader-option ${cityValue[columnIndex] === option.value ? 'active' : ''}`}
onClick={() => handleSelectOption(option, columnIndex)}
>
<Text className="option-text">{option.label}</Text>
</View>
))}
</ScrollView>
</Tabs.TabPane>
))}
</Tabs>
</View>
</Popup>
{/* Toast 组件 */}
<Toast id="toast" />
</View>
)
})

Loading…
Cancel
Save