Browse Source

门店详情

master
DU 5 months ago
parent
commit
90a02c0860
  1. 1
      src/app.config.ts
  2. BIN
      src/pageStore/images/billiardtab.png
  3. BIN
      src/pageStore/images/locationicon.png
  4. BIN
      src/pageStore/images/paitab.png
  5. BIN
      src/pageStore/images/payVip.png
  6. BIN
      src/pageStore/images/phone.png
  7. BIN
      src/pageStore/images/share.png
  8. BIN
      src/pageStore/images/storeBox.png
  9. 519
      src/pageStore/storeDetail/index.scss
  10. 472
      src/pageStore/storeDetail/index.tsx
  11. 10
      src/pages/store/index.tsx

1
src/app.config.ts

@ -16,6 +16,7 @@ export default defineAppConfig({ @@ -16,6 +16,7 @@ export default defineAppConfig({
'mapStore/index', // 地图找店页面
'reservation/index', // 预定页面
'appointmentStart/index', // 预约开台页面
'storeDetail/index', // 门店详情页面
]
},
{

BIN
src/pageStore/images/billiardtab.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
src/pageStore/images/locationicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
src/pageStore/images/paitab.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
src/pageStore/images/payVip.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
src/pageStore/images/phone.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/pageStore/images/share.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/pageStore/images/storeBox.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

519
src/pageStore/storeDetail/index.scss

@ -0,0 +1,519 @@ @@ -0,0 +1,519 @@
/**
* 门店详情页面样式
*/
.store_detail_page {
min-height: 100vh;
background-color: #121212;
position: relative;
// 自定义透明导航栏
.custom_nav_bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: transparent;
.nav_content {
display: flex;
align-items: center;
padding: 0 30px;
.nav_buttons {
display: flex;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 32px;
overflow: hidden;
.nav_btn {
display: flex;
align-items: center;
justify-content: center;
width: 66px;
height: 64px;
.nav_icon {
width: 36px;
height: 36px;
color: #FFFFFF;
&.back_icon {
transform: rotate(180deg);
}
&.home_icon {
width: 36px;
height: 36px;
}
}
}
.nav_divider {
width: 1px;
height: 36px;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}
// 轮播图区域
.swiper_container {
width: 100%;
height: 465px;
position: relative;
.swiper {
width: 100%;
height: 100%;
}
.swiper_image {
width: 100%;
height: 100%;
display: block;
}
.swiper_indicator {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
}
}
// 内容区域
.content_area {
position: relative;
margin-top: -20px;
background-color: #121212;
border-top-left-radius: 25px;
border-top-right-radius: 25px;
padding: 30px;
padding-bottom: 50px;
// 门店信息
.store_info_section {
margin-bottom: 33px;
.store_header {
display: flex;
align-items: center;
justify-content: space-between;
.store_name {
font-size: 42px;
color: #FFFFFF;
font-weight: 500;
}
.store_actions {
display: flex;
align-items: center;
gap: 30px;
.action_icon {
width: 33px;
height: 33px;
&.phone,
&.share {
width: 33px;
height: 33px;
}
}
}
}
.store_location {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 30px 0;
.location_left {
display: flex;
align-items: flex-start;
gap: 10px;
flex: 1;
.location_icon {
width: 16px;
height: 20px;
flex-shrink: 0;
margin-top: 5px;
}
.location_text {
font-size: 25px;
color: #E5E5E5;
line-height: 1.5;
flex: 1;
}
}
.distance_text {
font-size: 25px;
color: #E5E5E5;
flex-shrink: 0;
margin-left: 20px;
}
}
.store_price {
display: flex;
gap: 30px;
.price_text {
font-size: 25px;
color: #B8B8B8;
}
}
}
// VIP 横幅
.vip_banner {
width: 690px;
height: 83px;
position: relative;
margin-bottom: 30px;
.vip_bg {
width: 100%;
height: 100%;
border-radius: 10px;
}
.vip_btn {
position: absolute;
right: 30px;
top: 50%;
transform: translateY(-50%);
width: 158px;
height: 58px;
background: #614929;
border-radius: 29px;
display: flex;
align-items: center;
justify-content: center;
.vip_btn_text {
font-size: 25px;
color: #E0C699;
}
}
}
// Tab 导航
.tab_container {
display: flex;
justify-content: space-around;
padding: 20px 0;
margin-bottom: 30px;
.tab_item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.tab_count{
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 10px;
.count_label {
font-size: 26px;
color: #FFFFFF;
}
.count_value {
font-size: 26px;
color: #01CA68;
}
}
.tab_name {
font-size: 36px;
color: #FFFFFF;
}
.tab_indicator {
width: 29px;
height: 8px;
background: linear-gradient(-30deg, #0099E6, #33FF99);
border-radius: 4px;
margin-top: 10px;
}
}
}
// 台球和棋牌区域
.tables_section {
padding: 30px;
background: #202029;
border-radius: 13px;
// 分类区块
.category_block {
margin-bottom: 30px;
}
// 分类头部
.category_header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
.category_name {
font-size: 33px;
color: #FFFFFF;
font-weight: 500;
}
.category_price {
display: flex;
gap: 20px;
.vip_price {
font-size: 23px;
color: #F5CFA9;
}
.original_price,
.time_range,
.price_info {
font-size: 23px;
color: #999999;
}
}
}
// 台球桌网格
.tables_grid {
display: grid;
gap: 20px;
&.billiard_grid {
grid-template-columns: repeat(2, 1fr);
}
&.chess_grid {
grid-template-columns: repeat(2, 1fr);
}
}
// 台球桌卡片
.table_card {
position: relative;
border-radius: 12px;
overflow: hidden;
&.billiard_card {
width: 100%;
height: 204px;
}
&.chess_card {
width: 100%;
height: 300px;
}
.table_bg {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
// 蒙版
.table_mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
z-index: 1;
}
// 状态标签棋牌
.status_tag {
position: absolute;
top: 15px;
left: 15px;
padding: 5px 12px;
border-radius: 4px;
z-index: 2;
&.available {
background-color: #03C175;
}
&.using {
background-color: #FF4D4F;
}
.tag_text {
font-size: 21px;
color: #FFFFFF;
}
}
// 内容区域
.table_content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 2;
.table_name {
font-size: 33px;
color: #FFFFFF;
font-weight: bold;
margin-bottom: 15px;
text-align: center;
}
.table_buttons {
display: flex;
background-color: #00B05C;
border-radius: 33px;
overflow: hidden;
.table_btn {
font-size: 27px;
color: #FFFFFF;
padding: 15px 30px;
background-color: transparent;
position: relative;
// 中间分隔线
&:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 60%;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
// 单个按钮样式
.table_btn_single {
font-size: 27px;
color: #FFFFFF;
padding: 15px 36px;
border-radius: 33px;
background-color: #00B05C;
}
.table_status {
font-size: 25px;
color: #FFFFFF;
text-align: center;
white-space: pre-line;
line-height: 1.6;
}
}
}
// 棋牌区域
.chess_section {
margin-top: 40px;
}
}
// 存杆柜区域
.storage_section {
margin-top: 40px;
.section_title {
font-size: 33px;
color: #FFFFFF;
font-weight: 500;
margin-bottom: 20px;
display: block;
}
.storage_card {
width: 100%;
height: 255px;
position: relative;
border-radius: 12px;
overflow: hidden;
.storage_bg {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.storage_content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 30px;
display: flex;
flex-direction: column;
justify-content: flex-start;
z-index: 1;
.storage_status {
font-size: 29px;
color: #FFFFFF;
margin-bottom: 10px;
}
.storage_price {
font-size: 25px;
color: #03C175;
margin-bottom: 20px;
}
.storage_btn {
width: 120px;
height: 50px;
background-color: #21212A;
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
.btn_text {
font-size: 27px;
color: #03C175;
}
}
}
}
}
// 底部提示
.end_tip {
display: block;
text-align: center;
font-size: 25px;
color: #666666;
margin-top: 40px;
}
}
}

472
src/pageStore/storeDetail/index.tsx

@ -0,0 +1,472 @@ @@ -0,0 +1,472 @@
/**
*
*/
import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { useState, useEffect } from 'react'
import { Swiper } from '@taroify/core'
import { Arrow, HomeOutlined } from '@taroify/icons'
import './index.scss'
// 页面配置 - 隐藏导航栏
definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: ''
})
// 图片资源
import phoneIcon from '../images/phone.png'
import locationIcon from '../images/locationicon.png'
import shareIcon from '../images/share.png'
import payVipBg from '../images/payVip.png'
import billiardTabBg from '../images/billiardtab.png'
import paiTabBg from '../images/paitab.png'
import storeBoxBg from '../images/storeBox.png'
import swBG from '../../assets/big/swBG.png'
// 台球桌状态类型
type TableStatus = 'available' | 'using' | 'waiting' | 'reserved'
// 台球桌数据接口
interface BilliardTable {
id: string
name: string
status: TableStatus
waitTime?: string
buttons: Array<{
type: 'start' | 'reserve'
text: string
}>
}
// 台球桌分类数据
interface BilliardCategory {
name: string
vipPrice: string
originalPrice: string
tables: BilliardTable[]
}
// 棋牌桌数据接口
interface ChessTable {
id: string
name: string
status: TableStatus
statusText?: string
buttonText?: string
waitTime?: string
}
// 存杆柜数据接口
interface StorageCabinet {
id: string
status: 'available' | 'rented'
price: string
}
const StoreDetail = () => {
const [activeTab, setActiveTab] = useState(0) // 0: 中式台球, 1: 棋牌, 2: 存杆柜
const [statusBarHeight, setStatusBarHeight] = useState(20) // 状态栏高度
const [navBarHeight, setNavBarHeight] = useState(44) // 导航栏内容高度
// 获取系统信息设置导航栏高度
useEffect(() => {
const systemInfo = Taro.getSystemInfoSync()
setStatusBarHeight(systemInfo.statusBarHeight || 20)
// 微信小程序胶囊按钮信息
if (process.env.TARO_ENV === 'weapp') {
const menuButtonInfo = Taro.getMenuButtonBoundingClientRect()
const navHeight = (menuButtonInfo.top - (systemInfo.statusBarHeight || 0)) * 2 + menuButtonInfo.height
setNavBarHeight(navHeight)
}
}, [])
// 返回上一页
const handleBack = () => {
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
Taro.navigateBack()
} else {
Taro.switchTab({ url: '/pages/index/index' })
}
}
// 返回首页
const handleGoHome = () => {
Taro.switchTab({ url: '/pages/index/index' })
}
// 门店信息
const storeInfo = {
name: '北京黑八大师联盟球厅店',
address: '北京市XXX区XXX街道XXX路XXXXXX大厦101',
distance: '10.05km',
billiardPrice: '¥10.00起',
chessPrice: '¥20.00起'
}
// Tab 数据
const tabs = [
{ name: '中式台球', idleCount: 5 },
{ name: '棋牌', idleCount: 1 },
{ name: '存杆柜', idleCount: 0 }
]
// 台球桌分类数据
const [billiardCategories] = useState<BilliardCategory[]>([
{
name: '独牙&来力.山岩',
vipPrice: '¥8.00/小时',
originalPrice: '¥10.00/小时',
tables: [
{ id: '1', name: '独牙01', status: 'available', buttons: [{ type: 'start', text: '立即开台' }] },
{ id: '2', name: '独牙02', status: 'waiting', waitTime: '00 时 15 分', buttons: [] },
{ id: '3', name: '来力黑金刚03', status: 'available', buttons: [{ type: 'start', text: '立即开台' }] },
{ id: '4', name: '来力黑金刚04', status: 'using', buttons: [] }
]
},
{
name: '乔氏赛台',
vipPrice: '¥16.00/小时',
originalPrice: '¥20.00/小时',
tables: [
{ id: '5', name: '乔氏01', status: 'available', buttons: [{ type: 'start', text: '立即开台' }] },
{ id: '6', name: '乔氏02', status: 'available', buttons: [{ type: 'reserve', text: '预定' }, { type: 'start', text: '立即开台' }] },
{ id: '7', name: '乔氏03', status: 'available', buttons: [{ type: 'start', text: '立即开台' }] }
]
}
])
// 棋牌数据
const [chessTables] = useState<ChessTable[]>([
{ id: '1', name: 'Q01号', status: 'available', buttonText: '立即开台' },
{ id: '2', name: 'Q02号', status: 'using', statusText: '预计 16:00 结束' },
{ id: '3', name: 'Q03号', status: 'waiting', statusText: '预计等待:', waitTime: '00 时 15 分' },
{ id: '4', name: 'Q04号', status: 'reserved', buttonText: '预约开台' }
])
// 棋牌价格信息
const chessInfo = {
timeRange: '00:00-24:00',
price: '¥60.00/小时'
}
// 存杆柜数据
const [storageCabinet] = useState<StorageCabinet>({
id: '1',
status: 'available',
price: '1元/天'
})
useLoad(() => {
console.log('门店详情页面加载')
})
// 拨打电话
const handlePhoneCall = () => {
Taro.makePhoneCall({
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
}
// 分享
const handleShare = () => {
Taro.showToast({
title: '分享功能开发中',
icon: 'none'
})
}
// 开通VIP
const handleOpenVip = () => {
Taro.navigateTo({
url: '/pagesUser/vipBenefits/index'
})
}
// Tab 点击,滚动到对应位置
const handleTabClick = (index: number) => {
setActiveTab(index)
// 获取对应区域的 ID
const sectionIds = ['billiard-section', 'chess-section', 'storage-section']
const query = Taro.createSelectorQuery()
query.select(`#${sectionIds[index]}`).boundingClientRect()
query.select('#scroll-content').scrollOffset()
query.exec((res) => {
if (res[0] && res[1]) {
const targetTop = res[0].top + res[1].scrollTop - 150 // 减去 tab 高度
Taro.pageScrollTo({
scrollTop: targetTop,
duration: 300
})
}
})
}
// 进入预定页面
const handleReserve = (tableId: string) => {
Taro.navigateTo({
url: '/pageStore/reservation/index?tableId=' + tableId
})
}
// 进入开台页面
const handleStartTable = (tableId: string) => {
Taro.navigateTo({
url: '/pageScan/startTable/index?tableId=' + tableId
})
}
// 进入预约开台页面
const handleAppointmentStart = (tableId: string) => {
Taro.navigateTo({
url: '/pageStore/appointmentStart/index?tableId=' + tableId
})
}
// 进入存杆柜租赁页面
const handleCabinetRental = () => {
Taro.navigateTo({
url: '/pagesOrder/cabinetRental/index'
})
}
// 获取台球桌状态文本
const getBilliardStatusText = (table: BilliardTable) => {
if (table.status === 'using') return '使用中'
if (table.status === 'waiting') return `预计等待:\n${table.waitTime}`
return ''
}
// 获取棋牌状态标签
const getChessStatusTag = (status: TableStatus) => {
if (status === 'available') return { text: '空闲中', class: 'available' }
if (status === 'using') return { text: '使用中', class: 'using' }
if (status === 'waiting') return { text: '使用中', class: 'using' }
if (status === 'reserved') return { text: '空闲中', class: 'available' }
return { text: '', class: '' }
}
return (
<View className="store_detail_page">
{/* 自定义透明导航栏 */}
<View
className="custom_nav_bar"
style={{ paddingTop: `${statusBarHeight}px`, height: `${statusBarHeight + navBarHeight}px` }}
>
<View className="nav_content" style={{ height: `${navBarHeight}px` }}>
<View className="nav_buttons">
<View className="nav_btn" onClick={handleBack}>
<Arrow className="nav_icon back_icon" />
</View>
{/* <View className="nav_divider" />
<View className="nav_btn" onClick={handleGoHome}>
<HomeOutlined className="nav_icon home_icon" />
</View> */}
</View>
</View>
</View>
{/* 轮播图区域 */}
<View className="swiper_container">
<Swiper
className="swiper"
autoplay={3000}
lazyRender
>
<Swiper.Indicator className="swiper_indicator" />
<Swiper.Item>
<Image
className="swiper_image"
src={swBG}
mode="aspectFill"
/>
</Swiper.Item>
<Swiper.Item>
<Image
className="swiper_image"
src={swBG}
mode="aspectFill"
/>
</Swiper.Item>
</Swiper>
</View>
{/* 内容区域 */}
<View className="content_area" id="scroll-content">
{/* 门店信息 */}
<View className="store_info_section">
<View className="store_header">
<Text className="store_name">{storeInfo.name}</Text>
<View className="store_actions">
<Image className="action_icon phone" src={phoneIcon} mode="aspectFit" onClick={handlePhoneCall} />
<Image className="action_icon share" src={shareIcon} mode="aspectFit" onClick={handleShare} />
</View>
</View>
<View className="store_location">
<View className="location_left">
<Image className="location_icon" src={locationIcon} mode="aspectFit" />
<Text className="location_text">{storeInfo.address}</Text>
</View>
<Text className="distance_text">{storeInfo.distance}</Text>
</View>
<View className="store_price">
<Text className="price_text"> {storeInfo.billiardPrice}</Text>
<Text className="price_text"> {storeInfo.chessPrice}</Text>
</View>
</View>
{/* VIP 开通横幅 */}
<View className="vip_banner" onClick={handleOpenVip}>
<Image className="vip_bg" src={payVipBg} mode="aspectFill" />
<View className="vip_btn">
<Text className="vip_btn_text"></Text>
</View>
</View>
{/* Tab 导航 */}
<View className="tab_container">
{tabs.map((tab, index) => (
<View
key={index}
className={`tab_item ${activeTab === index ? 'active' : ''}`}
onClick={() => handleTabClick(index)}
>
<View className="tab_count">
<Text className="count_label"></Text>
<Text className="count_value">{tab.idleCount}</Text>
</View>
<Text className="tab_name">{tab.name}</Text>
{activeTab === index && <View className="tab_indicator" />}
</View>
))}
</View>
{/* 台球和棋牌区域 */}
<View className="tables_section">
{/* 台球区域 */}
<View id="billiard-section" className="billiard_section">
{billiardCategories.map((category, catIndex) => (
<View key={catIndex} className="category_block">
<View className="category_header">
<Text className="category_name">{category.name}</Text>
<View className="category_price">
<Text className="vip_price">VIP: {category.vipPrice}</Text>
<Text className="original_price">: {category.originalPrice}</Text>
</View>
</View>
<View className="tables_grid billiard_grid">
{category.tables.map((table) => (
<View
key={table.id}
className={`table_card billiard_card ${table.status !== 'available' ? 'disabled' : ''}`}
>
<Image className="table_bg" src={billiardTabBg} mode="aspectFill" />
{table.status !== 'available' && <View className="table_mask" />}
<View className="table_content">
<Text className="table_name">{table.name}</Text>
{table.status === 'available' && table.buttons.length > 0 ? (
table.buttons.length === 1 ? (
// 单个按钮
<Text
className="table_btn_single"
onClick={() => table.buttons[0].type === 'reserve' ? handleReserve(table.id) : handleStartTable(table.id)}
>
{table.buttons[0].text}
</Text>
) : (
// 按钮组
<View className="table_buttons">
{table.buttons.map((btn, btnIndex) => (
<Text
key={btnIndex}
className="table_btn"
onClick={() => btn.type === 'reserve' ? handleReserve(table.id) : handleStartTable(table.id)}
>
{btn.text}
</Text>
))}
</View>
)
) : (
<Text className="table_status">{getBilliardStatusText(table)}</Text>
)}
</View>
</View>
))}
</View>
</View>
))}
</View>
{/* 棋牌区域 */}
<View id="chess-section" className="chess_section">
<View className="category_header">
<Text className="category_name"></Text>
<View className="category_price">
<Text className="time_range">{chessInfo.timeRange}</Text>
<Text className="price_info">: {chessInfo.price}</Text>
</View>
</View>
<View className="tables_grid chess_grid">
{chessTables.map((table) => (
<View
key={table.id}
className={`table_card chess_card ${table.status === 'using' || table.status === 'waiting' ? 'disabled' : ''}`}
>
<Image className="table_bg" src={paiTabBg} mode="aspectFill" />
{(table.status === 'using' || table.status === 'waiting') && <View className="table_mask" />}
{/* 状态标签 */}
<View className={`status_tag ${getChessStatusTag(table.status).class}`}>
<Text className="tag_text">{getChessStatusTag(table.status).text}</Text>
</View>
<View className="table_content">
<Text className="table_name">{table.name}</Text>
{table.status === 'available' ? (
<Text className="table_btn_single" onClick={() => handleStartTable(table.id)}>{table.buttonText}</Text>
) : table.status === 'reserved' ? (
<Text className="table_btn_single" onClick={() => handleAppointmentStart(table.id)}>{table.buttonText}</Text>
) : (
<Text className="table_status">
{table.statusText}
{table.status === 'waiting' && `\n${table.waitTime}`}
</Text>
)}
</View>
</View>
))}
</View>
</View>
</View>
{/* 存杆柜区域 */}
<View id="storage-section" className="storage_section">
<Text className="section_title"></Text>
<View className="storage_card" onClick={handleCabinetRental}>
<Image className="storage_bg" src={storeBoxBg} mode="aspectFill" />
<View className="storage_content">
<Text className="storage_status">/</Text>
<Text className="storage_price">{storageCabinet.price}</Text>
<View className="storage_btn">
<Text className="btn_text"></Text>
</View>
</View>
</View>
</View>
{/* 底部提示 */}
<Text className="end_tip">~</Text>
</View>
</View>
)
}
export default StoreDetail

10
src/pages/store/index.tsx

@ -216,6 +216,14 @@ const StoreIndex = observer(() => { @@ -216,6 +216,14 @@ const StoreIndex = observer(() => {
}, 500)
}
// 跳转到店铺详情
const clickStoreItem = (store: any) => {
Taro.navigateTo({
url: '/pageStore/storeDetail/index?id=' + store.id
})
console.log('跳转到店铺详情', store)
}
// 跳转到地图找店页面(分包页面)
const handleGoToMap = () => {
Taro.navigateTo({
@ -266,7 +274,7 @@ const StoreIndex = observer(() => { @@ -266,7 +274,7 @@ const StoreIndex = observer(() => {
{/* 门店列表 */}
<View className="store-list">
{storeList.map((store) => (
<View className="store-item" key={store.id}>
<View className="store-item" key={store.id} onClick={() => clickStoreItem(store)}>
{/* 标题和价格 */}
<View className="store-header">
<Text className="store-name">{store.name}</Text>

Loading…
Cancel
Save