6 changed files with 440 additions and 17 deletions
@ -0,0 +1,94 @@ |
|||||||
|
.qr-handler-page { |
||||||
|
min-height: 100vh; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
background-color: #f5f5f5; |
||||||
|
padding: 40px 20px; |
||||||
|
|
||||||
|
// 加载状态 |
||||||
|
&.loading-state { |
||||||
|
.loading-content { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
gap: 20px; |
||||||
|
|
||||||
|
.loading-text { |
||||||
|
font-size: 32px; |
||||||
|
color: #666; |
||||||
|
margin-top: 20px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 错误状态 |
||||||
|
&.error-state { |
||||||
|
.error-content { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
text-align: center; |
||||||
|
max-width: 600px; |
||||||
|
|
||||||
|
.error-icon { |
||||||
|
font-size: 120px; |
||||||
|
margin-bottom: 30px; |
||||||
|
} |
||||||
|
|
||||||
|
.error-title { |
||||||
|
font-size: 36px; |
||||||
|
font-weight: bold; |
||||||
|
color: #333; |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.error-message { |
||||||
|
font-size: 28px; |
||||||
|
color: #666; |
||||||
|
line-height: 1.5; |
||||||
|
margin-bottom: 60px; |
||||||
|
} |
||||||
|
|
||||||
|
.error-actions { |
||||||
|
display: flex; |
||||||
|
gap: 20px; |
||||||
|
width: 100%; |
||||||
|
|
||||||
|
.action-btn { |
||||||
|
flex: 1; |
||||||
|
height: 88px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
border-radius: 12px; |
||||||
|
font-size: 32px; |
||||||
|
font-weight: 500; |
||||||
|
cursor: pointer; |
||||||
|
transition: all 0.3s ease; |
||||||
|
|
||||||
|
&.primary { |
||||||
|
background-color: #667eea; |
||||||
|
color: white; |
||||||
|
|
||||||
|
&:active { |
||||||
|
background-color: #5a6fd8; |
||||||
|
transform: scale(0.98); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&.secondary { |
||||||
|
background-color: white; |
||||||
|
color: #666; |
||||||
|
border: 2px solid #e0e0e0; |
||||||
|
|
||||||
|
&:active { |
||||||
|
background-color: #f5f5f5; |
||||||
|
transform: scale(0.98); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,119 @@ |
|||||||
|
/** |
||||||
|
* 二维码处理页面 |
||||||
|
* 统一处理所有扫码后的跳转逻辑 |
||||||
|
* |
||||||
|
* 使用场景: |
||||||
|
* 1. 微信扫码跳转到小程序时的落地页 |
||||||
|
* 2. 小程序内扫码后的统一处理页 |
||||||
|
*/ |
||||||
|
|
||||||
|
import { View, Text } from '@tarojs/components' |
||||||
|
import Taro, { useLoad, useRouter } from '@tarojs/taro' |
||||||
|
import { useState } from 'react' |
||||||
|
import { Loading } from '@taroify/core' |
||||||
|
import { parseQRCode, handleQRCodeNavigation, QRCodeType } from '@/utils/qrCodeHandler' |
||||||
|
import './index.scss' |
||||||
|
|
||||||
|
// 页面配置
|
||||||
|
definePageConfig({ |
||||||
|
navigationBarTitleText: '处理中...', |
||||||
|
navigationStyle: 'default' |
||||||
|
}) |
||||||
|
|
||||||
|
const QRHandlerPage = () => { |
||||||
|
const router = useRouter() |
||||||
|
const [loading, setLoading] = useState(true) |
||||||
|
const [error, setError] = useState<string>('') |
||||||
|
|
||||||
|
useLoad(async () => { |
||||||
|
console.log('二维码处理页面加载,参数:', router.params) |
||||||
|
|
||||||
|
try { |
||||||
|
// 从路由参数获取二维码内容
|
||||||
|
const { q, scene, qrContent } = router.params |
||||||
|
|
||||||
|
let content = '' |
||||||
|
|
||||||
|
// 处理不同来源的二维码内容
|
||||||
|
if (q) { |
||||||
|
// 微信扫码进入小程序的情况,q 参数是 base64 编码的 URL
|
||||||
|
content = decodeURIComponent(q) |
||||||
|
} else if (scene) { |
||||||
|
// 小程序码的场景值
|
||||||
|
content = scene |
||||||
|
} else if (qrContent) { |
||||||
|
// 直接传递的二维码内容
|
||||||
|
content = decodeURIComponent(qrContent) |
||||||
|
} else { |
||||||
|
throw new Error('未找到二维码内容') |
||||||
|
} |
||||||
|
|
||||||
|
console.log('解析二维码内容:', content) |
||||||
|
|
||||||
|
// 延迟一下,给用户看到处理中的状态
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500)) |
||||||
|
|
||||||
|
// 解析并处理二维码
|
||||||
|
const qrData = parseQRCode(content) |
||||||
|
console.log('解析结果:', qrData) |
||||||
|
|
||||||
|
if (qrData.type === QRCodeType.UNKNOWN) { |
||||||
|
throw new Error('无法识别的二维码格式') |
||||||
|
} |
||||||
|
|
||||||
|
// 执行跳转逻辑
|
||||||
|
await handleQRCodeNavigation(qrData) |
||||||
|
|
||||||
|
} catch (error: any) { |
||||||
|
console.error('二维码处理失败:', error) |
||||||
|
setError(error.message || '二维码处理失败') |
||||||
|
setLoading(false) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
const handleBackHome = () => { |
||||||
|
Taro.switchTab({ |
||||||
|
url: '/pages/index/index' |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 重新扫码
|
||||||
|
const handleRescan = () => { |
||||||
|
Taro.navigateTo({ |
||||||
|
url: '/pages/scanCode/index' |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if (error) { |
||||||
|
return ( |
||||||
|
<View className="qr-handler-page error-state"> |
||||||
|
<View className="error-content"> |
||||||
|
<View className="error-icon">❌</View> |
||||||
|
<Text className="error-title">处理失败</Text> |
||||||
|
<Text className="error-message">{error}</Text> |
||||||
|
|
||||||
|
<View className="error-actions"> |
||||||
|
<View className="action-btn primary" onClick={handleRescan}> |
||||||
|
重新扫码 |
||||||
|
</View> |
||||||
|
<View className="action-btn secondary" onClick={handleBackHome}> |
||||||
|
返回首页 |
||||||
|
</View> |
||||||
|
</View> |
||||||
|
</View> |
||||||
|
</View> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View className="qr-handler-page loading-state"> |
||||||
|
<View className="loading-content"> |
||||||
|
<Loading size="large" /> |
||||||
|
<Text className="loading-text">正在处理二维码...</Text> |
||||||
|
</View> |
||||||
|
</View> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default QRHandlerPage |
||||||
@ -0,0 +1,209 @@ |
|||||||
|
/** |
||||||
|
* 二维码处理工具 |
||||||
|
* 用于解析二维码内容并决定跳转逻辑 |
||||||
|
*/ |
||||||
|
|
||||||
|
import Taro from '@tarojs/taro' |
||||||
|
|
||||||
|
// 二维码类型枚举
|
||||||
|
export enum QRCodeType { |
||||||
|
TABLE_START = 'table_start', // 开台二维码
|
||||||
|
TABLE_CLOSE = 'table_close', // 关台二维码
|
||||||
|
CABINET = 'cabinet', // 存杆柜二维码
|
||||||
|
COUPON = 'coupon', // 团购券二维码
|
||||||
|
UNKNOWN = 'unknown' // 未知类型
|
||||||
|
} |
||||||
|
|
||||||
|
// 二维码数据接口
|
||||||
|
export interface QRCodeData { |
||||||
|
type: QRCodeType |
||||||
|
tableId?: string // 球桌ID
|
||||||
|
grpSn?: string // 设备编码
|
||||||
|
cabinetId?: string // 存杆柜ID
|
||||||
|
couponCode?: string // 团购券码
|
||||||
|
storeId?: string // 门店ID
|
||||||
|
[key: string]: any // 其他参数
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析二维码内容 |
||||||
|
* @param qrContent 二维码原始内容 |
||||||
|
* @returns 解析后的二维码数据 |
||||||
|
*/ |
||||||
|
export const parseQRCode = (qrContent: string): QRCodeData => { |
||||||
|
try { |
||||||
|
// 尝试解析 JSON 格式的二维码
|
||||||
|
if (qrContent.startsWith('{')) { |
||||||
|
const data = JSON.parse(qrContent) |
||||||
|
return { |
||||||
|
type: data.type || QRCodeType.UNKNOWN, |
||||||
|
...data |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 尝试解析 URL 格式的二维码
|
||||||
|
if (qrContent.startsWith('http')) { |
||||||
|
const url = new URL(qrContent) |
||||||
|
const params = new URLSearchParams(url.search) |
||||||
|
|
||||||
|
return { |
||||||
|
type: (params.get('type') as QRCodeType) || QRCodeType.UNKNOWN, |
||||||
|
tableId: params.get('tableId') || undefined, |
||||||
|
grpSn: params.get('grpSn') || undefined, |
||||||
|
cabinetId: params.get('cabinetId') || undefined, |
||||||
|
couponCode: params.get('couponCode') || undefined, |
||||||
|
storeId: params.get('storeId') || undefined, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 尝试解析简单格式:type:param1:param2
|
||||||
|
const parts = qrContent.split(':') |
||||||
|
if (parts.length >= 2) { |
||||||
|
const [type, ...params] = parts |
||||||
|
|
||||||
|
switch (type) { |
||||||
|
case 'table_start': |
||||||
|
return { |
||||||
|
type: QRCodeType.TABLE_START, |
||||||
|
grpSn: params[0], |
||||||
|
tableId: params[1], |
||||||
|
storeId: params[2] |
||||||
|
} |
||||||
|
case 'table_close': |
||||||
|
return { |
||||||
|
type: QRCodeType.TABLE_CLOSE, |
||||||
|
grpSn: params[0], |
||||||
|
tableId: params[1] |
||||||
|
} |
||||||
|
case 'cabinet': |
||||||
|
return { |
||||||
|
type: QRCodeType.CABINET, |
||||||
|
cabinetId: params[0], |
||||||
|
storeId: params[1] |
||||||
|
} |
||||||
|
case 'coupon': |
||||||
|
return { |
||||||
|
type: QRCodeType.COUPON, |
||||||
|
couponCode: params[0] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 无法识别的格式
|
||||||
|
return { |
||||||
|
type: QRCodeType.UNKNOWN, |
||||||
|
rawContent: qrContent |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('二维码解析失败:', error) |
||||||
|
return { |
||||||
|
type: QRCodeType.UNKNOWN, |
||||||
|
rawContent: qrContent |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 根据二维码数据执行跳转逻辑 |
||||||
|
* @param qrData 解析后的二维码数据 |
||||||
|
*/ |
||||||
|
export const handleQRCodeNavigation = async (qrData: QRCodeData) => { |
||||||
|
console.log('处理二维码跳转:', qrData) |
||||||
|
|
||||||
|
try { |
||||||
|
switch (qrData.type) { |
||||||
|
case QRCodeType.TABLE_START: |
||||||
|
// 开台二维码 - 跳转到开台选择页面
|
||||||
|
await Taro.navigateTo({ |
||||||
|
url: `/pageScan/startTable/index?grpSn=${qrData.grpSn}&tableId=${qrData.tableId}&storeId=${qrData.storeId || ''}` |
||||||
|
}) |
||||||
|
break |
||||||
|
|
||||||
|
case QRCodeType.TABLE_CLOSE: |
||||||
|
// 关台二维码 - 跳转到关台页面
|
||||||
|
await Taro.navigateTo({ |
||||||
|
url: `/pageScan/closeTable/index?grpSn=${qrData.grpSn}&tableId=${qrData.tableId}` |
||||||
|
}) |
||||||
|
break |
||||||
|
|
||||||
|
case QRCodeType.CABINET: |
||||||
|
// 存杆柜二维码 - 跳转到存杆柜页面
|
||||||
|
await Taro.navigateTo({ |
||||||
|
url: `/pagesOrder/poleStorage/index?cabinetId=${qrData.cabinetId}&storeId=${qrData.storeId || ''}` |
||||||
|
}) |
||||||
|
break |
||||||
|
|
||||||
|
case QRCodeType.COUPON: |
||||||
|
// 团购券二维码 - 跳转到团购券使用页面
|
||||||
|
await Taro.navigateTo({ |
||||||
|
url: `/pageScan/couponUse/index?couponCode=${qrData.couponCode}` |
||||||
|
}) |
||||||
|
break |
||||||
|
|
||||||
|
default: |
||||||
|
// 未知类型 - 显示错误提示
|
||||||
|
Taro.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: '无法识别的二维码,请确认二维码是否正确', |
||||||
|
showCancel: false |
||||||
|
}) |
||||||
|
break |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('页面跳转失败:', error) |
||||||
|
Taro.showToast({ |
||||||
|
title: '页面跳转失败', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 扫码并处理结果的完整流程 |
||||||
|
* @param options 扫码选项 |
||||||
|
*/ |
||||||
|
export const scanAndHandle = (options?: { |
||||||
|
onlyFromCamera?: boolean |
||||||
|
scanType?: ('qrCode' | 'barCode')[] |
||||||
|
}) => { |
||||||
|
return new Promise<QRCodeData>((resolve, reject) => { |
||||||
|
Taro.scanCode({ |
||||||
|
onlyFromCamera: options?.onlyFromCamera ?? true, |
||||||
|
scanType: options?.scanType ?? ['qrCode', 'barCode'], |
||||||
|
success: async (res) => { |
||||||
|
console.log('扫码成功:', res.result) |
||||||
|
|
||||||
|
// 解析二维码
|
||||||
|
const qrData = parseQRCode(res.result) |
||||||
|
|
||||||
|
// 执行跳转
|
||||||
|
await handleQRCodeNavigation(qrData) |
||||||
|
|
||||||
|
resolve(qrData) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
console.error('扫码失败:', err) |
||||||
|
|
||||||
|
if (err.errMsg?.includes('cancel')) { |
||||||
|
// 用户取消扫码
|
||||||
|
reject(new Error('用户取消扫码')) |
||||||
|
} else { |
||||||
|
// 扫码失败
|
||||||
|
Taro.showToast({ |
||||||
|
title: '扫码失败,请重试', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
reject(err) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 默认导出
|
||||||
|
export default { |
||||||
|
parseQRCode, |
||||||
|
handleQRCodeNavigation, |
||||||
|
scanAndHandle, |
||||||
|
QRCodeType |
||||||
|
} |
||||||
Loading…
Reference in new issue