diff --git a/config/env.ts b/config/env.ts index eac6adc..a141d2b 100644 --- a/config/env.ts +++ b/config/env.ts @@ -2,19 +2,19 @@ export const ENV_CONFIG = { // 开发环境 development: { - API_BASE_URL: 'https://cms-api.websoft.top/api', + API_BASE_URL: 'https://ysb-api.websoft.top/api', APP_NAME: '开发环境', DEBUG: 'true', }, // 生产环境 production: { - API_BASE_URL: 'https://cms-api.websoft.top/api', + API_BASE_URL: 'https://ysb-api.websoft.top/api', APP_NAME: '易赊宝', DEBUG: 'false', }, // 测试环境 test: { - API_BASE_URL: 'https://cms-api.websoft.top/api', + API_BASE_URL: 'https://ysb-api.websoft.top/api', APP_NAME: '测试环境', DEBUG: 'true', } diff --git a/src/app.config.ts b/src/app.config.ts index 52528b3..cd525a9 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -10,11 +10,14 @@ export default defineAppConfig({ "root": "passport", "pages": [ "login", - // "register", - // "forget", - // "setting", + "register", + "forget", + "setting", "agreement", - "sms-login" + "sms-login", + 'qr-login/index', + 'qr-confirm/index', + 'unified-qr/index' ] }, { diff --git a/src/passport/agreement.tsx b/src/passport/agreement.tsx index 5ce26d1..fbbae42 100644 --- a/src/passport/agreement.tsx +++ b/src/passport/agreement.tsx @@ -1,28 +1,47 @@ -import {useEffect, useState} from "react"; +import { useEffect, useState } from 'react' import Taro from '@tarojs/taro' -import {View, RichText} from '@tarojs/components' +import { Loading } from '@nutui/nutui-react-taro' +import { RichText, View } from '@tarojs/components' +import { getByCode } from '@/api/cms/cmsArticle' +import { wxParse } from '@/utils/common' const Agreement = () => { + const [loading, setLoading] = useState(true) + const [content, setContent] = useState('') - const [content, setContent] = useState('') - const reload = () => { - Taro.hideTabBar() - setContent('

' + - '欢迎使用' + - ' ' + - '【WebSoft】' + - '服务协议 ' + - '

') + const reload = async () => { + try { + Taro.hideTabBar() + } catch (_) { + // ignore (e.g. H5 / unsupported env) + } + + try { + const article = await getByCode('xieyi') + setContent(article?.content ? wxParse(article.content) : '

暂无协议内容

') + } catch (e) { + // Keep UI usable even if CMS/API fails. + // eslint-disable-next-line no-console + console.error('load agreement failed', e) + setContent('

协议内容加载失败

') + Taro.showToast({ title: '协议加载失败', icon: 'none' }) + } finally { + setLoading(false) + } } useEffect(() => { reload() }, []) + if (loading) { + return 加载中 + } + return ( <> - + ) diff --git a/src/passport/login.tsx b/src/passport/login.tsx index 76103cd..bec45c8 100644 --- a/src/passport/login.tsx +++ b/src/passport/login.tsx @@ -1,83 +1,13 @@ import {useEffect, useState} from "react"; import Taro from '@tarojs/taro' import {Input, Radio, Button} from '@nutui/nutui-react-taro' -import {loginBySms} from '@/api/passport/login' const Login = () => { const [isAgree, setIsAgree] = useState(false) - const [phone, setPhone] = useState('') - const [password, setPassword] = useState('') - const [loading, setLoading] = useState(false) - const reload = () => { Taro.hideTabBar() } - // 处理登录 - const handleLogin = async () => { - if (!isAgree) { - Taro.showToast({ - title: '请先同意服务协议', - icon: 'none' - }) - return - } - - if (!phone || phone.trim() === '') { - Taro.showToast({ - title: '请输入手机号', - icon: 'none' - }) - return - } - - if (!password || password.trim() === '') { - Taro.showToast({ - title: '请输入密码', - icon: 'none' - }) - return - } - - // 验证手机号格式 - const phoneRegex = /^1[3-9]\d{9}$/ - if (!phoneRegex.test(phone)) { - Taro.showToast({ - title: '请输入正确的手机号', - icon: 'none' - }) - return - } - - try { - setLoading(true) - await loginBySms({ - phone: phone, - code: password - }) - - Taro.showToast({ - title: '登录成功', - icon: 'success' - }) - - // 延迟跳转到首页 - setTimeout(() => { - Taro.reLaunch({ - url: '/pages/index/index' - }) - }, 1500) - } catch (error: any) { - console.error('登录失败:', error) - Taro.showToast({ - title: error.message || '登录失败,请重试', - icon: 'none' - }) - } finally { - setLoading(false) - } - } - useEffect(() => { reload() }, []) @@ -89,45 +19,24 @@ const Login = () => { <>
- setPhone(val)} - style={{backgroundColor: '#ffffff', borderRadius: '8px'}} - /> +
- setPassword(val)} - style={{backgroundColor: '#ffffff', borderRadius: '8px'}} - /> + +
+
+ Taro.navigateTo({url: '/passport/forget'})}>忘记密码 + Taro.navigateTo({url: '/passport/register'})}>立即注册
- {/*
*/} - {/* Taro.navigateTo({url: '/passport/forget'})}>忘记密码*/} - {/* Taro.navigateTo({url: '/passport/register'})}>立即注册*/} - {/*
*/}
- + +
+
+
- {/*
*/} - {/* */} - {/*
*/} {/*
*/} {/* 没有账号? Taro.navigateTo({url: '/passport/register'})}*/} {/* className={'text-blue-600'}>立即注册*/} diff --git a/src/passport/qr-confirm/index.config.ts b/src/passport/qr-confirm/index.config.ts new file mode 100644 index 0000000..9a9855a --- /dev/null +++ b/src/passport/qr-confirm/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '确认登录', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/passport/qr-confirm/index.tsx b/src/passport/qr-confirm/index.tsx new file mode 100644 index 0000000..184b27b --- /dev/null +++ b/src/passport/qr-confirm/index.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Button, Loading, Card } from '@nutui/nutui-react-taro'; +import { Success, Failure, Tips, User } from '@nutui/icons-react-taro'; +import Taro, { useRouter } from '@tarojs/taro'; +import { confirmQRLogin } from '@/api/passport/qr-login'; +import { useUser } from '@/hooks/useUser'; + +/** + * 扫码登录确认页面 + * 用于处理从二维码跳转过来的登录确认 + */ +const QRConfirmPage: React.FC = () => { + const router = useRouter(); + const { user, getDisplayName } = useUser(); + const [loading, setLoading] = useState(false); + const [confirmed, setConfirmed] = useState(false); + const [error, setError] = useState(''); + const [token, setToken] = useState(''); + + useEffect(() => { + // 从URL参数中获取token + const { qrCodeKey, token: urlToken } = router.params; + const loginToken = qrCodeKey || urlToken; + + if (loginToken) { + setToken(loginToken); + } else { + setError('无效的登录链接'); + } + }, [router.params]); + + // 确认登录 + const handleConfirmLogin = async () => { + if (!token) { + setError('缺少登录token'); + return; + } + + if (!user?.userId) { + setError('请先登录小程序'); + return; + } + + try { + setLoading(true); + setError(''); + + const result = await confirmQRLogin({ + token, + userId: user.userId, + platform: 'wechat', + wechatInfo: { + nickname: user.nickname, + avatar: user.avatar + } + }); + + if (result.success) { + setConfirmed(true); + Taro.showToast({ + title: '登录确认成功', + icon: 'success', + duration: 2000 + }); + + // 3秒后自动返回 + setTimeout(() => { + Taro.navigateBack(); + }, 3000); + } else { + setError(result.message || '登录确认失败'); + } + } catch (err: any) { + setError(err.message || '登录确认失败'); + } finally { + setLoading(false); + } + }; + + // 取消登录 + const handleCancel = () => { + Taro.navigateBack(); + }; + + // 重试 + const handleRetry = () => { + setError(''); + setConfirmed(false); + handleConfirmLogin(); + }; + + return ( + + + {/* 主要内容卡片 */} + + + {/* 图标 */} + + {loading ? ( + + + + ) : confirmed ? ( + + + + ) : error ? ( + + + + ) : ( + + + + )} + + + {/* 标题 */} + + {loading ? '正在确认登录...' : + confirmed ? '登录确认成功' : + error ? '登录确认失败' : '确认登录'} + + + {/* 描述 */} + + {loading ? '请稍候,正在为您确认登录' : + confirmed ? '您已成功确认登录,网页端将自动登录' : + error ? error : + `确认使用 ${getDisplayName()} 登录网页端?`} + + + {/* 用户信息 */} + {!loading && !confirmed && !error && user && ( + + + + + + + + {user.nickname || user.username || '用户'} + + + ID: {user.userId} + + + + + )} + + {/* 操作按钮 */} + + {loading ? ( + + ) : confirmed ? ( + + ) : error ? ( + + + + + ) : ( + + + + + )} + + + + + {/* 安全提示 */} + + + + + + + 安全提示 + + + 请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。 + + + + + + + + ); +}; + +export default QRConfirmPage; diff --git a/src/passport/qr-login/index.config.ts b/src/passport/qr-login/index.config.ts new file mode 100644 index 0000000..54abd28 --- /dev/null +++ b/src/passport/qr-login/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '扫码登录', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/passport/qr-login/index.tsx b/src/passport/qr-login/index.tsx new file mode 100644 index 0000000..c2b4321 --- /dev/null +++ b/src/passport/qr-login/index.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Card, Divider, Button } from '@nutui/nutui-react-taro'; +import { Scan, Success, Failure, Tips } from '@nutui/icons-react-taro'; +import Taro from '@tarojs/taro'; +import QRLoginScanner from '@/components/QRLoginScanner'; +import { useUser } from '@/hooks/useUser'; + +/** + * 扫码登录页面 + */ +const QRLoginPage: React.FC = () => { + const [loginHistory, setLoginHistory] = useState([]); + const { getDisplayName } = useUser(); + + // 处理扫码成功 + const handleScanSuccess = (result: any) => { + console.log('扫码登录成功:', result); + + // 添加到登录历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + userInfo: result.userInfo, + success: true + }; + setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 + + // 显示成功提示 + Taro.showToast({ + title: '登录确认成功', + icon: 'success', + duration: 2000 + }); + }; + + // 处理扫码失败 + const handleScanError = (error: string) => { + console.error('扫码登录失败:', error); + + // 添加到登录历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + error, + success: false + }; + setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); + }; + + // 返回上一页 + // const handleBack = () => { + // Taro.navigateBack(); + // }; + + // 清除历史记录 + const clearHistory = () => { + setLoginHistory([]); + Taro.showToast({ + title: '已清除历史记录', + icon: 'success' + }); + }; + + return ( + + {/* 导航栏 */} + {/*}*/} + {/* className="bg-white"*/} + {/*/>*/} + + {/* 主要内容 */} + + {/* 用户信息卡片 */} + + + + + + + + + {getDisplayName()} + + + 使用小程序扫码快速登录网页端 + + + + + {/* 扫码登录组件 */} + + + + + {/* 使用说明 */} + + + + + 使用说明 + + + 1. 在电脑或其他设备上打开网页端登录页面 + 2. 点击"扫码登录"按钮,显示登录二维码 + 3. 使用此功能扫描二维码即可快速登录 + 4. 扫码成功后,网页端将自动完成登录 + + + + + {/* 登录历史 */} + {loginHistory.length > 0 && ( + + + + 最近登录记录 + + + + + {loginHistory.map((record, index) => ( + + + + {record.success ? ( + + ) : ( + + )} + + + {record.success ? '登录成功' : '登录失败'} + + {record.error && ( + + {record.error} + + )} + + + + {record.time} + + + {index < loginHistory.length - 1 && ( + + )} + + ))} + + + + )} + + {/* 安全提示 */} + + + + + + + 安全提示 + + + 请确保只扫描来自官方网站的登录二维码,避免扫描来源不明的二维码,保护账户安全。 + + + + + + + + ); +}; + +export default QRLoginPage; diff --git a/src/passport/register.config.ts b/src/passport/register.config.ts index 77ed0bd..8018733 100644 --- a/src/passport/register.config.ts +++ b/src/passport/register.config.ts @@ -1,4 +1,5 @@ export default definePageConfig({ - navigationBarTitleText: '注册账号', + navigationBarTitleText: '注册/登录', navigationBarTextStyle: 'black' }) + diff --git a/src/passport/register.tsx b/src/passport/register.tsx index 553e0e0..2569e44 100644 --- a/src/passport/register.tsx +++ b/src/passport/register.tsx @@ -1,47 +1,295 @@ -import {useEffect, useState} from "react"; +import { useEffect, useMemo, useState } from 'react' import Taro from '@tarojs/taro' -import {Input, Radio, Button} from '@nutui/nutui-react-taro' +import { Button, Radio } from '@nutui/nutui-react-taro' +import { TenantId } from '@/config/app' +import { getUserInfo, getWxOpenId } from '@/api/layout' +import { saveStorageByLoginUser } from '@/utils/server' +import { + getStoredInviteParams, + parseInviteParams, + saveInviteParams, + trackInviteSource, + checkAndHandleInviteRelation, +} from '@/utils/invite' + +interface GetPhoneNumberDetail { + code?: string + encryptedData?: string + iv?: string + errMsg?: string +} + +interface GetPhoneNumberEvent { + detail: GetPhoneNumberDetail +} + +interface LoginResponse { + data: { + code?: number + message?: string + data?: { + access_token: string + user: any + } + } +} + +async function getWeappLoginCode(): Promise { + try { + const res = await new Promise<{ code?: string }>((resolve, reject) => { + Taro.login({ + success: (r) => resolve(r as any), + fail: (e) => reject(e), + }) + }) + return res?.code + } catch (_e) { + return undefined + } +} + +async function ensureWxOpenIdSaved(opts: { user?: any; wxLoginCode?: string }) { + // JSAPI 微信支付必须有 openid;注册/登录后立刻补齐,避免后续创建支付单失败。 + try { + if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) return + } catch (_e) { + if (process.env.TARO_ENV !== 'weapp') return + } + + if (opts.user?.openid) return + + const code = opts.wxLoginCode || (await getWeappLoginCode()) + if (!code) return + + // 该接口一般会在服务端把 openid 绑定到当前登录用户;返回值并不一定包含 openid。 + await getWxOpenId({ code }) + + // 同步本地 User(让后续页面/逻辑能直接读到 openid) + try { + const fresh = await getUserInfo() + if (fresh) Taro.setStorageSync('User', fresh) + } catch (_e) { + // ignore: openid 已在服务端绑定,本地不同步也不影响后端创建支付订单 + } +} + +function safeDecodeMaybeEncoded(input?: string): string { + if (!input) return '' + try { + // Taro 路由参数通常是 URL 编码过的字符串 + return decodeURIComponent(input) + } catch (_e) { + return input + } +} + +function isTabBarUrl(url: string) { + const pure = url.split('?')[0] + return ( + pure === '/pages/index/index' || + pure === '/pages/cart/cart' || + pure === '/pages/user/user' || + pure === '/pages/category/index' + ) +} const Register = () => { const [isAgree, setIsAgree] = useState(false) - const reload = () => { - Taro.hideTabBar() - } + const [loading, setLoading] = useState(false) + + // 短信验证码登录仅在非微信小程序端展示 + const isWeapp = useMemo(() => { + try { + return Taro.getEnv() === Taro.ENV_TYPE.WEAPP + } catch (_e) { + return process.env.TARO_ENV === 'weapp' + } + }, []) + + const router = Taro.getCurrentInstance().router useEffect(() => { - reload() + // 注册/登录页不需要展示 tabBar + Taro.hideTabBar() }, []) + const redirectUrl = useMemo(() => { + const raw = (router?.params as any)?.redirect as string | undefined + const decoded = safeDecodeMaybeEncoded(raw) + if (!decoded) return '' + return decoded.startsWith('/') ? decoded : `/${decoded}` + }, [router?.params]) + + // 如果从分享/二维码直接进入注册页(携带 inviter/source/t),先暂存邀请信息 + useEffect(() => { + try { + const inviteParams = parseInviteParams({ query: router?.params }) + if (inviteParams?.inviter) { + saveInviteParams(inviteParams) + trackInviteSource(inviteParams.source || 'qrcode', parseInt(inviteParams.inviter, 10)) + } + } catch (e) { + console.error('注册页处理邀请参数失败:', e) + } + }, [router?.params]) + + const navigateAfterLogin = async () => { + if (!redirectUrl) { + await Taro.reLaunch({ url: '/pages/index/index' }) + return + } + + if (isTabBarUrl(redirectUrl)) { + // switchTab 不支持携带 query,这里按纯路径跳转 + await Taro.switchTab({ url: redirectUrl.split('?')[0] }) + return + } + + // 替换当前注册页,避免返回栈里再回到注册页 + await Taro.redirectTo({ url: redirectUrl }) + } + + const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => { + if (!isAgree) { + Taro.showToast({ title: '请先勾选同意协议', icon: 'none' }) + return + } + if (loading) return + + const { code: phoneCode, encryptedData, iv, errMsg } = detail || {} + if (!phoneCode || (errMsg && errMsg.includes('fail'))) { + Taro.showToast({ title: '未授权手机号', icon: 'none' }) + return + } + + try { + setLoading(true) + + // 获取存储的邀请参数(推荐人ID) + const inviteParams = getStoredInviteParams() + const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter, 10) : 0 + + // 获取小程序登录 code(用于后续绑定 openid) + const wxLoginCode = await getWeappLoginCode() + + const res = (await Taro.request({ + url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone', + method: 'POST', + data: { + code: phoneCode, + encryptedData, + iv, + notVerifyPhone: true, + refereeId: refereeId, + sceneType: 'save_referee', + tenantId: TenantId, + }, + header: { + 'content-type': 'application/json', + TenantId, + }, + })) as unknown as LoginResponse + + if ((res as any)?.data?.code === 1) { + Taro.showToast({ title: res.data.message || '登录失败', icon: 'none' }) + return + } + + const token = res?.data?.data?.access_token + const user = res?.data?.data?.user + if (!token || !user?.userId) { + Taro.showToast({ title: '登录失败,请重试', icon: 'none' }) + return + } + + saveStorageByLoginUser(token, user) + + // 注册/登录成功后,立即补齐 openid(JSAPI 支付必需) + try { + await ensureWxOpenIdSaved({ user, wxLoginCode }) + } catch (e) { + console.error('注册页绑定 openid 失败:', e) + } + + // 登录成功后尝试绑定推荐关系(如果有待处理 inviter,会自动处理并清理参数) + try { + await checkAndHandleInviteRelation() + } catch (e) { + console.error('注册页登录后处理邀请关系失败:', e) + } + + Taro.showToast({ title: '登录成功', icon: 'success' }) + setTimeout(() => { + navigateAfterLogin().catch((e) => console.error('登录后跳转失败:', e)) + }, 800) + } catch (e: any) { + console.error('注册/登录失败:', e) + Taro.showToast({ title: e?.message || '登录失败', icon: 'none' }) + } finally { + setLoading(false) + } + } + + const goSmsLogin = () => { + const inviteParams = getStoredInviteParams() + const inviter = inviteParams?.inviter + const source = inviteParams?.source + const t = inviteParams?.t + + const params: Record = {} + if (redirectUrl) params.redirect = redirectUrl + // 兜底:把 inviter 带过去,避免“先点注册再进入”时丢失 + if (inviter) params.inviter = inviter + if (source) params.source = source + if (t) params.t = t + + const qs = Object.entries(params) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) + .join('&') + Taro.navigateTo({ url: `/passport/sms-login${qs ? `?${qs}` : ''}` }) + } + return ( <> -
-
免费试用14天,快速上手独立站
-
建站、选品、营销、支付、物流,全部搞定
-
- WebSoft为您提供独立站的解决方案,提供专业、高效、安全的运营服务。 +
+
注册/登录
+ +
+ + + {!isWeapp && ( + + )}
-
- + +
+ setIsAgree(!isAgree)} /> + setIsAgree(!isAgree)}> + 勾选表示您已阅读并同意 + + Taro.navigateTo({ url: '/passport/agreement' })} + className={'text-blue-600'} + > + 《服务协议及隐私政策》 +
-
- -
-
- -
-
- -
-
- setIsAgree(!isAgree)}> - setIsAgree(!isAgree)}>勾选表示您已阅读并同意 - Taro.navigateTo({url: '/passport/agreement'})} className={'text-blue-600'}>《服务协议及隐私政策》 -
-
- ) } + export default Register diff --git a/src/passport/sms-login.tsx b/src/passport/sms-login.tsx index 62da43d..7b3dad1 100644 --- a/src/passport/sms-login.tsx +++ b/src/passport/sms-login.tsx @@ -3,6 +3,7 @@ import Taro from '@tarojs/taro' import {Input, Button} from '@nutui/nutui-react-taro' import {loginBySms, sendSmsCaptcha} from "@/api/passport/login"; import {LoginParam} from "@/api/passport/login/model"; +import {checkAndHandleInviteRelation, hasPendingInvite, parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite"; const SmsLogin = () => { const [loading, setLoading] = useState(false) @@ -13,6 +14,46 @@ const SmsLogin = () => { code: '' }) + const router = Taro.getCurrentInstance().router + const redirectParam = (router?.params as any)?.redirect as string | undefined + + const safeDecodeMaybeEncoded = (input?: string) => { + if (!input) return '' + try { + return decodeURIComponent(input) + } catch (_e) { + return input + } + } + + const redirectUrl = (() => { + const decoded = safeDecodeMaybeEncoded(redirectParam) + if (!decoded) return '' + return decoded.startsWith('/') ? decoded : `/${decoded}` + })() + + const isTabBarUrl = (url: string) => { + const pure = url.split('?')[0] + return ( + pure === '/pages/index/index' || + pure === '/pages/cart/cart' || + pure === '/pages/user/user' || + pure === '/pages/category/index' + ) + } + + const navigateAfterLogin = async () => { + if (!redirectUrl) { + await Taro.reLaunch({ url: '/pages/index/index' }) + return + } + if (isTabBarUrl(redirectUrl)) { + await Taro.switchTab({ url: redirectUrl.split('?')[0] }) + return + } + await Taro.redirectTo({ url: redirectUrl }) + } + const reload = () => { Taro.hideTabBar() } @@ -21,6 +62,19 @@ const SmsLogin = () => { reload() }, []) + // 如果从分享/二维码链接进入短信登录页,先暂存邀请信息 + useEffect(() => { + try { + const inviteParams = parseInviteParams({ query: router?.params }) + if (inviteParams?.inviter) { + saveInviteParams(inviteParams) + trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter, 10)) + } + } catch (e) { + console.error('短信登录页处理邀请参数失败:', e) + } + }, [router?.params]) + // 倒计时效果 useEffect(() => { let timer: NodeJS.Timeout @@ -131,6 +185,15 @@ const SmsLogin = () => { code: formData.code }) + // 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定 + if (hasPendingInvite()) { + try { + await checkAndHandleInviteRelation() + } catch (e) { + console.error('短信登录后处理邀请关系失败:', e) + } + } + Taro.showToast({ title: '登录成功', icon: 'success' @@ -138,8 +201,9 @@ const SmsLogin = () => { // 延迟跳转到首页 setTimeout(() => { - Taro.reLaunch({ - url: '/pages/index/index' + navigateAfterLogin().catch((e) => { + console.error('短信登录后跳转失败:', e) + Taro.reLaunch({ url: '/pages/index/index' }) }) }, 1500) diff --git a/src/passport/unified-qr/index.config.ts b/src/passport/unified-qr/index.config.ts new file mode 100644 index 0000000..3a5a194 --- /dev/null +++ b/src/passport/unified-qr/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '统一扫码', + navigationBarTextStyle: 'black' +} \ No newline at end of file diff --git a/src/passport/unified-qr/index.tsx b/src/passport/unified-qr/index.tsx new file mode 100644 index 0000000..be5cda1 --- /dev/null +++ b/src/passport/unified-qr/index.tsx @@ -0,0 +1,342 @@ +import React, { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Card, Button, Tag } from '@nutui/nutui-react-taro'; +import { Scan, Success, Failure, Tips, ArrowLeft } from '@nutui/icons-react-taro'; +import Taro from '@tarojs/taro'; +import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan'; + +/** + * 统一扫码页面 + * 支持登录和核销两种类型的二维码扫描 + */ +const UnifiedQRPage: React.FC = () => { + const [scanHistory, setScanHistory] = useState([]); + const { + startScan, + isLoading, + canScan, + state, + result, + error, + scanType, + reset + } = useUnifiedQRScan(); + + // 处理扫码成功 + const handleScanSuccess = (result: UnifiedScanResult) => { + console.log('扫码成功:', result); + + // 添加到扫码历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + type: result.type, + data: result.data, + message: result.message, + success: true + }; + setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 + + // 根据类型给出不同提示 + if (result.type === ScanType.VERIFICATION) { + // 核销成功后询问是否继续扫码 + setTimeout(() => { + Taro.showModal({ + title: '核销成功', + content: '是否继续扫码核销其他水票/礼品卡?', + success: (res) => { + if (res.confirm) { + handleStartScan(); + } + } + }); + }, 2000); + } + }; + + // 处理扫码失败 + const handleScanError = (error: string) => { + console.error('扫码失败:', error); + + // 添加到扫码历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + error, + success: false + }; + setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 + }; + + // 开始扫码 + const handleStartScan = async () => { + try { + const scanResult = await startScan(); + if (scanResult) { + handleScanSuccess(scanResult); + } + } catch (error: any) { + handleScanError(error.message || '扫码失败'); + } + }; + + // 返回上一页 + const handleGoBack = () => { + Taro.navigateBack(); + }; + + // 获取状态图标 + const getStatusIcon = (success: boolean, type?: ScanType) => { + console.log(type,'获取状态图标') + if (success) { + return ; + } else { + return ; + } + }; + + // 获取类型标签 + const getTypeTag = (type: ScanType) => { + switch (type) { + case ScanType.LOGIN: + return 登录; + case ScanType.VERIFICATION: + return 核销; + default: + return 未知; + } + }; + + return ( + + {/* 页面头部 */} + + + + 统一扫码 + + 支持登录和核销功能 + + + + + {/* 主要扫码区域 */} + + + {/* 状态显示 */} + {state === 'idle' && ( + <> + + + 智能扫码 + + + 自动识别登录和核销二维码 + + + + )} + + {state === 'scanning' && ( + <> + + + 扫码中... + + + 请对准二维码 + + + + )} + + {state === 'processing' && ( + <> + + + + + 处理中... + + + {scanType === ScanType.LOGIN ? '正在确认登录' : + scanType === ScanType.VERIFICATION ? '正在核销' : '正在处理'} + + + )} + + {state === 'success' && result && ( + <> + + + {result.message} + + {result.type === ScanType.VERIFICATION && result.data && ( + + {result.data.businessType === 'gift' && result.data.gift && ( + <> + + 礼品:{result.data.gift.goodsName || result.data.gift.name || '未知'} + + + 面值:¥{result.data.gift.faceValue} + + + )} + {result.data.businessType === 'ticket' && result.data.ticket && ( + <> + + 水票:{result.data.ticket.templateName || '水票'} + + + 本次核销:{result.data.qty || 1} 次 + + + 剩余可用:{result.data.ticket.availableQty ?? 0} 次 + + + )} + + )} + + + + + + )} + + {state === 'error' && ( + <> + + + 操作失败 + + + {error || '未知错误'} + + + + + + + )} + + + + {/* 扫码历史 */} + {scanHistory.length > 0 && ( + + + + 最近扫码记录 + + + {scanHistory.map((record, index) => ( + + + {getStatusIcon(record.success, record.type)} + + + {record.type && getTypeTag(record.type)} + + {record.time} + + + + {record.success ? record.message : record.error} + + {record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'gift' && record.data?.gift && ( + + {record.data.gift.goodsName || record.data.gift.name} - ¥{record.data.gift.faceValue} + + )} + {record.success && record.type === ScanType.VERIFICATION && record.data?.businessType === 'ticket' && record.data?.ticket && ( + + {record.data.ticket.templateName || '水票'} - 本次核销 {record.data.qty || 1} 次 + + )} + + + + ))} + + + )} + + {/* 功能说明 */} + + + + + + + 功能说明 + + + • 登录二维码:自动确认网页端登录 + + + • 核销二维码:核销用户水票/礼品卡 + + + • 系统会自动识别二维码类型并执行相应操作 + + + + + + + ); +}; + +export default UnifiedQRPage;