Browse Source

新版本接口,对接, 包括商店列表和用户详情等

master
DU 1 month ago
parent
commit
b212dc8764
  1. 3
      package.json
  2. 36
      src/api/basicInfo.ts
  3. 9
      src/api/customer.ts
  4. 56
      src/api/startDevice.ts
  5. 15
      src/app.ts
  6. 28
      src/pageScan/closeTable/components/FooterButtons.tsx
  7. 26
      src/pageScan/selfStart/index.tsx
  8. 143
      src/pages/store/index.tsx
  9. 8
      src/pages/user/index.tsx
  10. 35
      src/types/startDevice.ts

3
package.json

@ -28,8 +28,7 @@
"dev:jd": "npm run build:jd -- --watch" "dev:jd": "npm run build:jd -- --watch"
}, },
"browserslist": [ "browserslist": [
"defaults and fully supports es6-module", "chrome >= 67"
"maintained node versions"
], ],
"author": "", "author": "",
"dependencies": { "dependencies": {

36
src/api/basicInfo.ts

@ -0,0 +1,36 @@
/**
* API
* /api/v2/BasicInfo/
*/
import { get } from '../services/request'
/**
*
*/
export interface StoreInfoDto {
id: number
sn: string
name: string
/** 店类型:0-重点,1-普通 */
type: number
typeName: string
/** 是否加盟:0-否,1-是 */
isUnion: number
address: string
manager: string
phone: string
inst_Sn: string
instName: string
rgn_Sn: string
regionName: string
longItude: number
latItude: number
createTime: string
}
/**
*
*/
export const getStoreList = () => {
return get<StoreInfoDto[]>('/BasicInfo/GetAllBpInfo')
}

9
src/api/customer.ts

@ -4,6 +4,15 @@
import { get, put } from '../services/request' import { get, put } from '../services/request'
import { CustomerDto, UpdateCustomerDto } from '../types/login' import { CustomerDto, UpdateCustomerDto } from '../types/login'
/**
* OpenId
* getCustomerBySn sn
* @param openId OpenId
*/
export const getCustomerByOpenId = (openId: string) => {
return get<CustomerDto>(`/Customer/${openId}`)
}
/** /**
* SN * SN
* @param sn CustomerDto.sn * @param sn CustomerDto.sn

56
src/api/startDevice.ts

@ -14,6 +14,7 @@ import {
PayType, PayType,
StopGroupDevRequestDto, StopGroupDevRequestDto,
StopGroupDevResponseDto, StopGroupDevResponseDto,
CheckOpenStatusResponse,
} from '../types/startDevice' } from '../types/startDevice'
/** 小程序 JSAPI 支付固定交易类型 */ /** 小程序 JSAPI 支付固定交易类型 */
@ -218,6 +219,58 @@ export const getTablePrice = (openId: string, groupDevSn: string) => {
return post<number>('/TablesDev/get-price', data) return post<number>('/TablesDev/get-price', data)
} }
/**
*
* "付款成功但 scanType 仍为 open"
* @param outTradeNo outTradeNo
*/
export const checkOpenStatus = (outTradeNo: string) => {
return get<CheckOpenStatusResponse>('/TablesDev/check-open-status', { outTradeNo })
}
/**
* /
* @param outTradeNo
* @param maxRetries 8 2s 16s
* @param intervalMs
* @returns operationId/ null
*/
export const pollCheckOpenStatus = async (
outTradeNo: string,
maxRetries = 8,
intervalMs = 2000
): Promise<CheckOpenStatusResponse | null> => {
for (let i = 0; i < maxRetries; i++) {
await new Promise(resolve => setTimeout(resolve, intervalMs))
try {
const res = await checkOpenStatus(outTradeNo)
const { isOpened, openStatus } = res.data
if (isOpened && openStatus === 'Opened') {
return res.data
}
if (openStatus === 'Failed') {
console.warn('开台状态查询:台子开启失败')
return null
}
} catch (e) {
console.warn(`查询开台状态第 ${i + 1} 次异常:`, e)
}
}
console.warn('查询开台状态超时')
return null
}
/**
*
* @param orderSn opInfo.orderNo
*/
export const updateShareStatus = (orderSn: string) => {
return post<boolean>('/TablesDev/UpdateShareStatus', { orderSn }, {
showLoading: true,
loadingText: '处理中...',
})
}
// 导出枚举类型 // 导出枚举类型
export { CtrlType, PayType } export { CtrlType, PayType }
@ -233,6 +286,9 @@ export default {
immediateClose, immediateClose,
renewFee, renewFee,
turnOffRestart, turnOffRestart,
checkOpenStatus,
pollCheckOpenStatus,
updateShareStatus,
getTablePrice, getTablePrice,
getTableInfo, getTableInfo,
CtrlType, CtrlType,

15
src/app.ts

@ -2,13 +2,14 @@ import { PropsWithChildren } from 'react'
import { useLaunch } from '@tarojs/taro' import { useLaunch } from '@tarojs/taro'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { userStore } from './store' import { userStore } from './store'
import { getCustomerByOpenId } from './api/customer'
import './app.scss' import './app.scss'
function App({ children }: PropsWithChildren<any>) { function App({ children }: PropsWithChildren<any>) {
useLaunch(() => { useLaunch(() => {
console.log('台球馆小程序启动') console.log('台球馆小程序启动')
// 初始化:从本地存储恢复用户信息 // 先从本地缓存恢复用户信息(快速)
try { try {
const savedUserInfo = Taro.getStorageSync('userInfo') const savedUserInfo = Taro.getStorageSync('userInfo')
if (savedUserInfo) { if (savedUserInfo) {
@ -17,6 +18,18 @@ function App({ children }: PropsWithChildren<any>) {
} catch (error) { } catch (error) {
console.warn('初始化用户信息失败:', error) console.warn('初始化用户信息失败:', error)
} }
// 后台静默刷新(获取最新余额等信息),不阻塞启动
const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (openId) {
getCustomerByOpenId(openId)
.then(res => {
if (res.data) {
userStore.setUserInfoFromWechat(res.data)
}
})
.catch(e => console.warn('启动刷新用户信息失败:', e))
}
}) })
return children return children

28
src/pageScan/closeTable/components/FooterButtons.tsx

@ -4,15 +4,15 @@
import { View, Text } from '@tarojs/components' import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { immediateClose, invokeWechatPay } from '../../../api/startDevice' import { immediateClose, invokeWechatPay, updateShareStatus } from '../../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../../api/order' import { closeOrder, pollTablePaymentStatus } from '../../../api/order'
import { userStore } from '../../../store' import { userStore, tableStore } from '../../../store'
import './FooterButtons.scss' import './FooterButtons.scss'
interface FooterButtonsProps { interface FooterButtonsProps {
type: 'owner' | 'guest' // owner: 开台人员扫码, guest: 非开台人扫码 type: 'owner' | 'guest'
grpSn: string // 设备编码 grpSn: string
operationId: number // 操作记录ID operationId: number
} }
const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
@ -138,6 +138,21 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
}) })
} }
// 分享球台
const handleShareTable = async () => {
const orderNo = tableStore.scanInfo?.opInfo?.orderNo
if (!orderNo) {
Taro.showToast({ title: '订单编号暂不可用,请稍后再试', icon: 'none', duration: 2000 })
return
}
try {
await updateShareStatus(orderNo)
Taro.showToast({ title: '已开启分享', icon: 'success', duration: 2000 })
} catch (_) {
// 错误由 request 拦截器处理
}
}
// 邀请球友 // 邀请球友
const handleInviteFriend = () => { const handleInviteFriend = () => {
console.log('邀请球友') console.log('邀请球友')
@ -168,6 +183,9 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
<Text className="button-text"></Text> <Text className="button-text"></Text>
</View> </View>
<View className="secondary-buttons"> <View className="secondary-buttons">
<View className="secondary-button" onClick={handleShareTable}>
<Text className="secondary-text"></Text>
</View>
<View className="secondary-button" onClick={handleInviteFriend}> <View className="secondary-button" onClick={handleInviteFriend}>
<Text className="secondary-text"></Text> <Text className="secondary-text"></Text>
</View> </View>

26
src/pageScan/selfStart/index.tsx

@ -6,8 +6,8 @@ import Taro, { useLoad, useRouter } from '@tarojs/taro'
import { useState } from 'react' import { useState } from 'react'
import { observer } from 'mobx-react' import { observer } from 'mobx-react'
import { Stepper } from '@taroify/core' import { Stepper } from '@taroify/core'
import { depositStart, balanceStart, invokeWechatPay, PayType, getTableInfo } from '../../api/startDevice' import { depositStart, balanceStart, invokeWechatPay, PayType, getTableInfo, pollCheckOpenStatus } from '../../api/startDevice'
import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { closeOrder } from '../../api/order'
import { userStore, tableStore } from '../../store' import { userStore, tableStore } from '../../store'
import './index.scss' import './index.scss'
@ -152,30 +152,30 @@ const SelfStart = observer(() => {
try { try {
await invokeWechatPay(result.data.paymentResponseDto.payParameters) await invokeWechatPay(result.data.paymentResponseDto.payParameters)
// 轮询服务端确认支付结果 // 轮询台子是否真正开启(check-open-status),解决付款后仍显示 open 的问题
Taro.showLoading({ title: '确认支付中...', mask: true }) Taro.showLoading({ title: '开台确认中...', mask: true })
const paid = await pollTablePaymentStatus(consumptionId) const openResult = await pollCheckOpenStatus(outTradeNo!)
Taro.hideLoading() Taro.hideLoading()
if (paid) { if (openResult?.isOpened) {
// 支付成功后重新拉取台桌信息,更新 scanInfo(opInfo 会有完整数据) // 用接口返回的 operationId(比 consumptionId 更准确)
const confirmedOperationId = openResult.operationId || consumptionId
// 刷新 scanInfo,关台页 opInfo 有真实数据
try { try {
const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') const openId2 = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
const freshInfo = await getTableInfo(grpSn, openId2) const freshInfo = await getTableInfo(grpSn, openId2)
tableStore.setScanInfo(freshInfo.data) tableStore.setScanInfo(freshInfo.data)
} catch (_) { } catch (_) {}
// 刷新失败不阻断流程,关台页会显示 '--' 但功能正常
}
Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 })
setTimeout(() => { setTimeout(() => {
Taro.redirectTo({ Taro.redirectTo({
url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${confirmedOperationId}`
}) })
}, 2000) }, 2000)
} else { } else {
Taro.showModal({ Taro.showModal({
title: '支付确认中', title: '开台确认中',
content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', content: '台桌正在启动,请稍后重新扫码进入关台页,如有疑问请联系店长。',
showCancel: false, showCancel: false,
confirmText: '知道了', confirmText: '知道了',
}) })

143
src/pages/store/index.tsx

@ -9,6 +9,7 @@ import { Popup, Toast, Tabs } from '@taroify/core'
import { useCascader } from '@taroify/hooks' import { useCascader } from '@taroify/hooks'
import { Arrow, Cross, Search } from '@taroify/icons' import { Arrow, Cross, Search } from '@taroify/icons'
import { appStore } from '../../store' import { appStore } from '../../store'
import { getStoreList, StoreInfoDto } from '../../api/basicInfo'
import storeIcon from '../../assets/icon/storeIcon.png' import storeIcon from '../../assets/icon/storeIcon.png'
import locationIcon from '../../assets/icon/locationIcon.png' import locationIcon from '../../assets/icon/locationIcon.png'
import './index.scss' import './index.scss'
@ -60,53 +61,14 @@ const cityOptions = [
}, },
] ]
// 模拟门店数据
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 StoreIndex = observer(() => {
const [selectedCity, setSelectedCity] = useState('选城市') const [selectedCity, setSelectedCity] = useState('选城市')
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
const [cityPopupOpen, setCityPopupOpen] = useState(false) const [cityPopupOpen, setCityPopupOpen] = useState(false)
const [storeList, setStoreList] = useState(mockStoreList) const [allStores, setAllStores] = useState<StoreInfoDto[]>([])
const [storeList, setStoreList] = useState<StoreInfoDto[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [isSearched, setIsSearched] = useState(false) // 是否已执行搜索 const [isSearched, setIsSearched] = useState(false)
const [cityValue, setCityValue] = useState<string[]>([]) const [cityValue, setCityValue] = useState<string[]>([])
const [tabIndex, setTabIndex] = useState(0) const [tabIndex, setTabIndex] = useState(0)
@ -119,8 +81,22 @@ const StoreIndex = observer(() => {
useLoad(() => { useLoad(() => {
console.log('门店页面加载') console.log('门店页面加载')
loadStores()
}) })
const loadStores = async () => {
setLoading(true)
try {
const res = await getStoreList()
setAllStores(res.data)
setStoreList(res.data)
} catch (e) {
console.error('获取门店列表失败:', e)
} finally {
setLoading(false)
}
}
// 监听弹窗状态,控制TabBar显示/隐藏 // 监听弹窗状态,控制TabBar显示/隐藏
useEffect(() => { useEffect(() => {
if (cityPopupOpen) { if (cityPopupOpen) {
@ -166,30 +142,14 @@ const StoreIndex = observer(() => {
// 搜索门店 // 搜索门店
const handleSearch = () => { const handleSearch = () => {
// 无内容时提示输入
if (!searchValue.trim()) { if (!searchValue.trim()) {
Toast.open('请输入门店名称或地址') Toast.open('请输入门店名称或地址')
return return
} }
setIsSearched(true)
setLoading(true) setStoreList(allStores.filter(store =>
Toast.loading({ store.name.includes(searchValue) || (store.address || '').includes(searchValue)
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)
} }
// 输入框变化 // 输入框变化
@ -200,28 +160,15 @@ const StoreIndex = observer(() => {
// 清除搜索并恢复默认数据 // 清除搜索并恢复默认数据
const handleClearSearch = () => { const handleClearSearch = () => {
setSearchValue('') setSearchValue('')
setIsSearched(false) // 切换回搜索图标 setIsSearched(false)
setStoreList(allStores)
// 恢复默认数据
setLoading(true)
Toast.loading({
message: '加载中...',
duration: 0,
})
setTimeout(() => {
setLoading(false)
Toast.close()
setStoreList(mockStoreList)
}, 500)
} }
// 跳转到店铺详情 // 跳转到店铺详情
const clickStoreItem = (store: any) => { const clickStoreItem = (store: StoreInfoDto) => {
Taro.navigateTo({ Taro.navigateTo({
url: '/pageStore/storeDetail/index?id=' + store.id url: '/pageStore/storeDetail/index?id=' + store.id
}) })
console.log('跳转到店铺详情', store)
} }
// 跳转到地图找店页面(分包页面) // 跳转到地图找店页面(分包页面)
@ -273,46 +220,50 @@ const StoreIndex = observer(() => {
{/* 门店列表 */} {/* 门店列表 */}
<View className="store-list"> <View className="store-list">
{storeList.map((store) => ( {loading ? (
<View className="list-footer"><Text className="footer-text">...</Text></View>
) : storeList.length === 0 ? (
<View className="list-footer"><Text className="footer-text"></Text></View>
) : (
storeList.map((store) => (
<View className="store-item" key={store.id} onClick={() => clickStoreItem(store)}> <View className="store-item" key={store.id} onClick={() => clickStoreItem(store)}>
{/* 标题和价格 */} {/* 标题和类型 */}
<View className="store-header"> <View className="store-header">
<Text className="store-name">{store.name}</Text> <Text className="store-name">{store.name}</Text>
<View className="store-price"> <View className="store-price">
<Text className="price-symbol">¥</Text> <Text className="price-value">{store.typeName || (store.isUnion === 1 ? '加盟店' : '直营店')}</Text>
<Text className="price-value">{store.price.toFixed(2)}</Text>
<Text className="price-unit">/</Text>
</View> </View>
</View> </View>
{/* 标签 */} {/* 标签 */}
<View className="store-tags"> <View className="store-tags">
<View className="tag tag-billiard"> {store.instName && (
{store.billiardCount} <View className="tag tag-billiard">{store.instName}</View>
</View> )}
<View className="tag tag-chess"> {store.regionName && (
{store.chessCount} <View className="tag tag-chess">{store.regionName}</View>
</View> )}
<View className="tag tag-type">
{store.type}
</View>
</View> </View>
{/* 地址和距离 */} {/* 地址和电话 */}
<View className="store-location"> <View className="store-location">
<View className="location-left"> <View className="location-left">
<Image className="location-icon" src={locationIcon} mode="aspectFit" /> <Image className="location-icon" src={locationIcon} mode="aspectFit" />
<Text className="address-text">{store.address}</Text> <Text className="address-text">{store.address || '地址未填写'}</Text>
</View> </View>
<Text className="distance-text">{store.distance} {'>'}</Text> {store.phone && (
<Text className="distance-text">{store.phone}</Text>
)}
</View> </View>
</View> </View>
))} ))
)}
{/* 列表底部提示 */} {!loading && storeList.length > 0 && (
<View className="list-footer"> <View className="list-footer">
<Text className="footer-text">~</Text> <Text className="footer-text">~</Text>
</View> </View>
)}
</View> </View>
</View> </View>

8
src/pages/user/index.tsx

@ -8,7 +8,7 @@ import { observer } from 'mobx-react'
import { Image as TaroifyImage, Button } from '@taroify/core' import { Image as TaroifyImage, Button } from '@taroify/core'
import { userStore } from '../../store' import { userStore } from '../../store'
import { wechatLogin } from '../../api/login' import { wechatLogin } from '../../api/login'
import { getCustomerBySn } from '../../api/customer' import { getCustomerByOpenId } from '../../api/customer'
import { getBigImage } from '@/utils/imageHelper' import { getBigImage } from '@/utils/imageHelper'
import './index.scss' import './index.scss'
@ -30,10 +30,10 @@ const UserIndex = observer(() => {
useLoad(async () => { useLoad(async () => {
console.log('个人中心页面加载') console.log('个人中心页面加载')
// 已登录时,刷新最新用户信息(余额、会员类型等) // 已登录时,刷新最新用户信息(余额、会员类型等)
const sn = userStore.userInfo?.sn const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId')
if (userStore.isLoggedIn && sn) { if (userStore.isLoggedIn && openId) {
try { try {
const res = await getCustomerBySn(sn) const res = await getCustomerByOpenId(openId)
if (res.data) { if (res.data) {
userStore.setUserInfoFromWechat(res.data) userStore.setUserInfoFromWechat(res.data)
} }

35
src/types/startDevice.ts

@ -333,3 +333,38 @@ export interface StopGroupDevResponseDto {
/** 商户订单号,前端轮询用 */ /** 商户订单号,前端轮询用 */
outTradeNo: string outTradeNo: string
} }
// ─── 查询开台状态接口(/api/v2/TablesDev/check-open-status)相关类型 ──────────
/**
*
* Pending Processing Opened / Failed
*/
export type OpenStatus = 'Pending' | 'Processing' | 'Opened' | 'Failed'
/**
* data
* GET /api/v2/TablesDev/check-open-status?outTradeNo=xxx
*/
export interface CheckOpenStatusResponse {
/** 商户订单号 */
outTradeNo: string
/** 是否已成功开台 */
isOpened: boolean
/** 开台状态:Pending-等待中、Processing-处理中、Opened-已开台、Failed-失败 */
openStatus: OpenStatus
/** 设备编码 */
grpSn: string
/** 设备工作状态:0-空闲,1-使用中,2-已预约,3-维护中,4-开台中 */
workStatus: number
/** 工作状态描述 */
workStatusDesc: string
/** 当前开台记录 ID(开台成功后有值,用于关台页) */
operationId: number
/** 开台时间 */
startTime: string
/** 预计结束时间(定时开台时有值) */
expectedEndTime: string
/** 开台人用户编码 */
userSn: string
}

Loading…
Cancel
Save