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

323 lines
11 KiB

/**
* 自助开台页面
*/
import { View, Text, Image } from '@tarojs/components'
import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react'
import { observer } from 'mobx-react'
import { Stepper } from '@taroify/core'
import { depositStart, balanceStart, invokeWechatPay, PayType, getTableInfo, pollCheckOpenStatus } from '../../api/startDevice'
import { closeOrder } from '../../api/order'
import { userStore, tableStore } from '../../store'
import './index.scss'
// 使用本地图片资源
const phoneIcon = require('../startTable/images/phone.png')
const SelfStart = observer(() => {
const router = useRouter()
const scanInfo = tableStore.scanInfo
const priceInfo = scanInfo?.price
const defaultDeposit = priceInfo?.deposit || 100
const [deposit, setDeposit] = useState(defaultDeposit) // 押金金额
const [paymentMethod, setPaymentMethod] = useState<'微信支付' | '余额支付'>('微信支付') // 支付方式
const [loading, setLoading] = useState(false) // 加载状态
// 从路由参数获取设备信息,如果没有则从 store 获取
const grpSn = tableStore.getGrpSn(router.params.grpSn)
// 门店余额从 userStore 获取
const storeBalance = userStore.balance || 0
useLoad(() => {
console.log('自助开台页面加载, 设备编码:', grpSn)
if (grpSn) {
tableStore.setCurrentGrpSn(grpSn)
}
setTimeout(() => {
Taro.showToast({ title: '开台后需要手动进行关台哦!', icon: 'none', duration: 2000 })
}, 300)
})
// 拨打电话
const handlePhoneCall = () => {
Taro.makePhoneCall({
phoneNumber: '100-666-XXXX',
}).catch((err) => {
console.error('拨打电话失败:', err)
})
}
// 押金变化
const handleDepositChange = (value: number) => {
setDeposit(value)
}
const pricePerHour = priceInfo?.currentPrice || priceInfo?.standard || 0
// 根据押金和实际价格计算预计时长
const calculateDuration = () => {
if (!pricePerHour) return '-'
const hours = Math.floor(deposit / pricePerHour)
const minutes = Math.round((deposit / pricePerHour - hours) * 60)
return `${hours}小时${minutes}分钟`
}
// 选择支付方式
const handleSelectPayment = () => {
// 判断余额是否充足
if (storeBalance >= deposit) {
Taro.showActionSheet({
itemList: ['微信支付', '余额支付'],
success: (res) => {
if (res.tapIndex === 0) {
setPaymentMethod('微信支付')
} else if (res.tapIndex === 1) {
setPaymentMethod('余额支付')
}
}
})
} else {
Taro.showToast({
title: '余额不足,仅支持微信支付',
icon: 'none',
duration: 2000
})
}
}
// 去充值
const handleRecharge = () => {
console.log('去充值')
Taro.showToast({
title: '功能开发中',
icon: 'none',
duration: 2000
})
}
// 立即开台
const handleConfirmStart = async () => {
if (loading) return
// 获取用户 openId
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (!openId) {
Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
})
return
}
if (!grpSn) {
Taro.showToast({
title: '设备信息异常,请重新扫码',
icon: 'none',
duration: 2000
})
return
}
setLoading(true)
try {
console.log('立即开台,押金:', deposit, '设备编码:', grpSn, '支付方式:', paymentMethod)
// 根据支付方式选择接口
let result
if (paymentMethod === '余额支付') {
// 余额开台
result = await balanceStart(grpSn, openId, deposit)
} else {
// 押金开台(微信支付)
result = await depositStart(grpSn, openId, deposit, PayType.WechatPay)
}
console.log('开台接口返回:', result)
// 保存操作记录ID到 store
if (result.data.consumptionId) {
tableStore.setCurrentOperationId(result.data.consumptionId)
}
const consumptionId = result.data.consumptionId
const outTradeNo = result.data.paymentResponseDto?.outTradeNo
// 判断是否需要支付
if (result.data.paymentResponseDto?.payParameters) {
// 调起微信支付
try {
await invokeWechatPay(result.data.paymentResponseDto.payParameters)
// 轮询台子是否真正开启(check-open-status),解决付款后仍显示 open 的问题
Taro.showLoading({ title: '开台确认中...', mask: true })
const openResult = await pollCheckOpenStatus(outTradeNo!)
Taro.hideLoading()
if (openResult?.isOpened) {
// 用接口返回的 operationId(比 consumptionId 更准确)
const confirmedOperationId = openResult.operationId || consumptionId
// 刷新 scanInfo,关台页 opInfo 有真实数据
try {
const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
const freshInfo = await getTableInfo(grpSn, openId2)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => {
Taro.redirectTo({
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${confirmedOperationId}`
})
}, 2000)
} else {
Taro.showModal({
title: '开台确认中',
content: '台桌正在启动,请稍后重新扫码进入关台页,如有疑问请联系店长。',
showCancel: false,
confirmText: '知道了',
})
}
} catch (payError: any) {
console.log('支付错误:', payError)
// 关闭服务端未支付订单,避免产生悬挂订单
if (outTradeNo) closeOrder(outTradeNo).catch(() => {})
if (payError.errMsg?.includes('cancel')) {
Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 })
} else {
Taro.showToast({ title: payError.errMsg || '支付失败', icon: 'none', duration: 2000 })
}
}
} else {
// 无需支付(余额支付等场景)
try {
const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
const freshInfo = await getTableInfo(grpSn, openId2)
tableStore.setScanInfo(freshInfo.data)
} catch (_) {
// 刷新失败不阻断流程
}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => {
Taro.redirectTo({
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}`
})
}, 2000)
}
} catch (error: any) {
console.error('开台失败:', error)
// 错误提示由 request 拦截器处理
} finally {
setLoading(false)
}
}
return (
<View className="self-start-page">
{/* 内容区域 */}
<View className="content-area">
{/* 台球桌信息卡片 */}
<View className="table-card">
{/* 顶部:店名和电话 */}
<View className="card-header">
<Text className="store-name">{scanInfo?.shopName || '--'}</Text>
<View className="phone-box" onClick={handlePhoneCall}>
<Image className="phone-icon" src={phoneIcon} mode="aspectFit" />
<Text className="phone-text"></Text>
</View>
</View>
{/* 台球桌标题 */}
<View className="table-title">
<Text>{scanInfo?.tableInfo?.name || '--'}</Text>
</View>
{/* 价格信息 */}
<View className="price-info">
<View className="price-item">
<Text className="price-value">¥{pricePerHour > 0 ? pricePerHour.toFixed(2) : '--'}</Text>
<Text className="price-label">/</Text>
</View>
<View className="price-item">
<Text className="price-label"></Text>
<Text className="price-value">¥{priceInfo ? priceInfo.deposit.toFixed(2) : '--'}</Text>
</View>
</View>
{/* 价格时段 */}
<View className="time-period">
<Text>{priceInfo?.pricePeriod || '--'}</Text>
</View>
</View>
{/* 押金卡片 */}
<View className="deposit-card">
{/* 押金输入 */}
<View className="deposit-input-row">
<Text className="deposit-label"></Text>
<Stepper
value={deposit}
min={defaultDeposit}
step={10}
onChange={handleDepositChange}
shape="circular"
size="27px"
className="deposit-stepper"
/>
</View>
{/* 押金说明 */}
<Text className="deposit-tip">100退</Text>
{/* 分割线 */}
<View className="divider" />
{/* 续费时长 */}
<View className="duration-row">
<Text className="duration-label"></Text>
<Text className="duration-value">{calculateDuration()}</Text>
</View>
</View>
{/* 支付方式 */}
<View className="payment-cell" onClick={handleSelectPayment}>
<Text className="payment-label"></Text>
<View className="payment-right">
<Text className="payment-value">{paymentMethod}</Text>
{/* <Text className="arrow-icon">›</Text> */}
</View>
</View>
{/* 门店余额 */}
<View className="balance-cell">
<View className="balance-left">
<Text className="balance-label"></Text>
<Text className="balance-amount">
<Text className="balance-symbol"></Text>
{storeBalance.toFixed(2)}
</Text>
</View>
<View className="recharge-btn" onClick={handleRecharge}>
<Text className="recharge-text"></Text>
</View>
</View>
</View>
{/* 底部固定区域 */}
<View className="footer-bar">
<View className="payment-info">
<Text className="payment-label"></Text>
<Text className="payment-symbol"></Text>
<Text className="payment-amount">{deposit.toFixed(2)}</Text>
</View>
<View className="confirm-btn" onClick={handleConfirmStart}>
<Text className="confirm-text"></Text>
</View>
</View>
</View>
)
})
export default SelfStart