diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..3fe0207 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,36 @@ +/** + * 应用配置文件 + * 可根据环境变量配置不同的 API 地址 + */ + +// API 基础地址配置 +// 直接配置,如需修改请更改此处 +const API_BASE_URL = 'https://39.101.75.71:8889' + +// API 版本 +const API_VERSION = 'v2' + +// 完整的 API 前缀 +const API_PREFIX = `${API_BASE_URL}/api/${API_VERSION}` + +// 是否开启调试模式(可根据需要手动切换) +const DEBUG_MODE = true + +export const config = { + // API 基础地址 + apiBaseUrl: API_BASE_URL, + + // API 版本 + apiVersion: API_VERSION, + + // 完整 API 前缀 + apiPrefix: API_PREFIX, + + // 请求超时时间(毫秒) + timeout: 30000, + + // 是否开启调试模式 + debug: DEBUG_MODE, +} + +export default config diff --git a/src/pageScan/selfStart/index.tsx b/src/pageScan/selfStart/index.tsx index d3821a3..5ed7101 100644 --- a/src/pageScan/selfStart/index.tsx +++ b/src/pageScan/selfStart/index.tsx @@ -2,21 +2,27 @@ * 自助开台页面 */ import { View, Text, Image } from '@tarojs/components' -import Taro, { useLoad } from '@tarojs/taro' +import Taro, { useLoad, useRouter } from '@tarojs/taro' import { useState } from 'react' import { Stepper } from '@taroify/core' +import { startTable, invokeWechatPay, TableOperationType, PaymentChannel } from '../../services/tableApi' import './index.scss' // 使用本地图片资源 const phoneIcon = require('../startTable/images/phone.png') const SelfStart = () => { + const router = useRouter() const [deposit, setDeposit] = useState(100) // 押金金额 const [paymentMethod] = useState('微信支付') // 支付方式 const [storeBalance] = useState(20.00) // 门店余额 + const [loading, setLoading] = useState(false) // 加载状态 + + // 从路由参数获取设备信息 + const grpSn = router.params.grpSn || '' // 设备编码 useLoad(() => { - console.log('自助开台页面加载') + console.log('自助开台页面加载, 设备编码:', grpSn) // 显示提示 Toast setTimeout(() => { @@ -69,13 +75,107 @@ const SelfStart = () => { } // 立即开台 - const handleConfirmStart = () => { - console.log('立即开台,押金:', deposit) - Taro.showToast({ - title: '功能开发中', - icon: 'none', - duration: 2000 - }) + const handleConfirmStart = async () => { + if (loading) return + + // 获取用户 openId + // const openId = 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) + + // TODO: 暂时写假的测试下 + const params = { + grpSn:'123', + openId:'4221', + ctrlType: 1, // TableOperationType.SelfServiceDeposit, // 押金开台 + amount: 100,//deposit, + payType: 1,//PaymentChannel.WechatPay, // 微信支付 + durationHours: 3, + couponCode: '', + } + console.log('开台请求参数:', params) + // 调用开台接口 + const result = await startTable(params) + + console.log('开台接口返回:', result) + + // 判断是否需要支付 + if (result.data.payParameters) { + // 调起微信支付 + try { + await invokeWechatPay(result.data.payParameters) + + // 支付成功 + Taro.showToast({ + title: '开台成功', + icon: 'success', + duration: 2000 + }) + + // 跳转到关台页面或开台成功页面 + setTimeout(() => { + Taro.redirectTo({ + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.operationId}` + }) + }, 2000) + } catch (payError: any) { + + console.log('error:', payError) + // 用户取消支付或支付失败 + if (payError.errMsg?.includes('cancel')) { + Taro.showToast({ + title: '已取消支付', + icon: 'none', + duration: 2000 + }) + } else { + Taro.showToast({ + title: payError.errMsg || '支付失败', + icon: 'none', + duration: 2000 + }) + } + } + } else { + // 无需支付(余额支付等场景) + Taro.showToast({ + title: '开台成功', + icon: 'success', + duration: 2000 + }) + + setTimeout(() => { + Taro.redirectTo({ + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.operationId}` + }) + }, 2000) + } + } catch (error: any) { + console.error('开台失败:', error) + // 错误提示由 request 拦截器处理 + } finally { + setLoading(false) + } } return ( diff --git a/src/services/request.ts b/src/services/request.ts new file mode 100644 index 0000000..f1e23b7 --- /dev/null +++ b/src/services/request.ts @@ -0,0 +1,183 @@ +/** + * 请求封装 + * 基于 Taro.request 封装的 HTTP 请求工具 + */ +import Taro from '@tarojs/taro' +import config from '../config' + +// 请求配置接口 +interface RequestConfig { + url: string + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'TRACE' | 'CONNECT' + data?: any + header?: Record + timeout?: number + showLoading?: boolean + loadingText?: string + showError?: boolean +} + +// 响应结果接口 +interface ResponseResult { + message: string + code: number + success: boolean + data: T +} + +// 请求拦截器 +const requestInterceptor = (options: RequestConfig) => { + // 处理完整 URL + let url = options.url + if (!url.startsWith('http')) { + url = `${config.apiPrefix}${url}` + } + + // 默认请求头 + const header: Record = { + 'Content-Type': 'application/json', + ...options.header, + } + + // 获取 token(如果有) + const token = Taro.getStorageSync('token') + if (token) { + header['Authorization'] = `Bearer ${token}` + } + + // 获取 openId(如果有) + const openId = Taro.getStorageSync('openId') + if (openId) { + header['X-OpenId'] = openId + } + + return { + ...options, + url, + header, + } +} + +// 响应拦截器 +const responseInterceptor = (response: Taro.request.SuccessCallbackResult>): ResponseResult => { + const { statusCode, data } = response + + if (statusCode >= 200 && statusCode < 300) { + // 请求成功 + if (data.success) { + return data + } else { + // 业务错误 + throw { + code: data.code, + message: data.message || '请求失败', + data: data.data, + } + } + } else if (statusCode === 401) { + // 未授权,清除登录信息 + Taro.removeStorageSync('token') + Taro.removeStorageSync('openId') + throw { + code: 401, + message: '登录已过期,请重新登录', + } + } else if (statusCode === 400) { + // 参数错误 + throw { + code: data.code || 400, + message: data.message || '请求参数错误', + data: data.data, + } + } else if (statusCode === 500) { + // 服务器错误 + throw { + code: data.code || 500, + message: data.message || '服务器内部错误', + data: data.data, + } + } else { + throw { + code: statusCode, + message: `请求失败: ${statusCode}`, + } + } +} + +// 封装请求方法 +const request = async (options: RequestConfig): Promise> => { + const { + showLoading = false, + loadingText = '加载中...', + showError = true, + timeout = config.timeout, + ...restOptions + } = options + + // 显示加载提示 + if (showLoading) { + Taro.showLoading({ title: loadingText, mask: true }) + } + + try { + // 请求拦截 + const finalOptions = requestInterceptor(restOptions) + + if (config.debug) { + console.log('[Request]', finalOptions.method || 'GET', finalOptions.url, finalOptions.data) + } + + // 发起请求 + const response = await Taro.request>({ + ...finalOptions, + timeout, + }) + + if (config.debug) { + console.log('[Response]', response.statusCode, response.data) + } + + // 响应拦截 + return responseInterceptor(response) + } catch (error: any) { + if (config.debug) { + console.error('[Request Error]', error) + } + + // 显示错误提示 + if (showError) { + const errorMessage = error.message || error.errMsg || '网络请求失败' + Taro.showToast({ + title: errorMessage, + icon: 'none', + duration: 2000, + }) + } + + throw error + } finally { + // 隐藏加载提示 + if (showLoading) { + Taro.hideLoading() + } + } +} + +// 快捷方法 +export const get = (url: string, data?: any, options?: Partial) => { + return request({ url, method: 'GET', data, ...options }) +} + +export const post = (url: string, data?: any, options?: Partial) => { + return request({ url, method: 'POST', data, ...options }) +} + +export const put = (url: string, data?: any, options?: Partial) => { + return request({ url, method: 'PUT', data, ...options }) +} + +export const del = (url: string, data?: any, options?: Partial) => { + return request({ url, method: 'DELETE', data, ...options }) +} + +export default request diff --git a/src/services/tableApi.ts b/src/services/tableApi.ts new file mode 100644 index 0000000..551dec1 --- /dev/null +++ b/src/services/tableApi.ts @@ -0,0 +1,150 @@ +/** + * 球台相关 API + * 包含开台、关台、续费等接口 + */ +import { post } from './request' + +/** + * 开台操作类型 + */ +export enum TableOperationType { + /** 押金开台 */ + SelfServiceDeposit = 1, + /** 余额开台 */ + SelfServiceBalance = 2, + /** 定时开台 */ + Timed = 3, + /** 信用开台 */ + Credit = 4, + /** 团购券开台 */ + GroupBuy = 5, + /** 立即关台 */ + ImmediateClose = 6, + /** 续费 */ + Renew = 7, + /** 关灯重开 */ + ReopenAfterClose = 8, +} + +/** + * 支付渠道 + */ +export enum PaymentChannel { + /** 微信支付 */ + WechatPay = 1, + /** 支付宝 */ + Alipay = 2, + /** 银联支付 */ + UnionPay = 3, + /** 余额支付 */ + Balance = 4, + /** 银行卡支付 */ + BankCard = 5, +} + +/** + * 开台请求参数 + */ +export interface StartTableRequest { + /** 设备编码 */ + grpSn: string + /** 用户 OpenId */ + openId: string + /** 开台操作类型 */ + ctrlType: TableOperationType + /** 消费金额 */ + amount?: number + /** 支付渠道 */ + payType?: PaymentChannel + /** 开台时长(小时),定时开台使用 */ + durationHours?: number + /** 团购券码,团购券开台使用 */ + couponCode?: string +} + +/** + * 开台响应数据 + */ +export interface StartTableResponse { + /** 机构编码 */ + instSn: string + /** 店铺编码 */ + bpSn: string + /** 球台编码 */ + grpSn: string + /** 开台设备id */ + operationId: number + /** 开始时间 */ + startTime: string + /** 结束时间 */ + expectedEndTime: string + /** 开台操作详情id */ + detailIds: number[] + /** 控制id */ + controlMessageId: number + /** 商户订单号 */ + outTradeNo: string + /** 支付参数(用于调起微信支付) */ + payParameters: WechatPayParameters | null + /** 支付URL(用于扫码支付) */ + payUrl: string +} + +/** + * 微信支付参数 + */ +export interface WechatPayParameters { + /** 时间戳 */ + timeStamp: string + /** 随机字符串 */ + nonceStr: string + /** 统一下单接口返回的 prepay_id */ + package: string + /** 签名方式 */ + signType: string + /** 签名 */ + paySign: string +} + +/** + * 开台 API + * @param data 开台请求参数 + * @returns 开台响应 + */ +export const startTable = (data: StartTableRequest) => { + return post('/Tables/start', data, { + showLoading: true, + loadingText: '开台中...', + }) +} + +/** + * 调起微信支付 + * @param payParams 支付参数 + * @returns Promise + */ +export const invokeWechatPay = (payParams: WechatPayParameters): Promise => { + return new Promise((resolve, reject) => { + // 使用 wx.requestPayment 调起微信支付 + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType as 'MD5' | 'HMAC-SHA256' | 'RSA', + paySign: payParams.paySign, + success: () => { + resolve() + }, + fail: (err) => { + reject(err) + }, + }) + }) +} + +export default { + startTable, + invokeWechatPay, + TableOperationType, + PaymentChannel, +}