diff --git a/project.private.config.json b/project.private.config.json index ebc893c..599dec3 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -1,7 +1,26 @@ { "libVersion": "3.15.2", "projectname": "websopy-mp", - "condition": {}, + "condition": { + "miniprogram": { + "list": [ + { + "name": "pages/passport/qr-confirm/index", + "pathName": "pages/passport/qr-confirm/index", + "query": "", + "scene": null, + "launchMode": "default" + }, + { + "name": "passport/qr-confirm/index", + "pathName": "passport/qr-confirm/index", + "query": "", + "launchMode": "default", + "scene": null + } + ] + } + }, "setting": { "urlCheck": true, "coverView": true, diff --git a/src/app.config.ts b/src/app.config.ts index 4f4a9e9..e0dd0c4 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -18,6 +18,7 @@ export default { "phone-auth/index", 'qr-login/index', 'qr-confirm/index', + 'invite/index', 'unified-qr/index', 'webview/index' ] diff --git a/src/passport/invite/index.config.ts b/src/passport/invite/index.config.ts new file mode 100644 index 0000000..421e049 --- /dev/null +++ b/src/passport/invite/index.config.ts @@ -0,0 +1,5 @@ +export default definePageConfig({ + navigationBarTitleText: '邀请加入', + navigationStyle: 'custom', + backgroundColor: '#0a0a1a', +}) diff --git a/src/passport/invite/index.tsx b/src/passport/invite/index.tsx new file mode 100644 index 0000000..8d70a8a --- /dev/null +++ b/src/passport/invite/index.tsx @@ -0,0 +1,338 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text, Button, Image } from '@tarojs/components'; +import Taro, { useRouter } from '@tarojs/taro'; +import { SERVER_API_URL } from "@/utils/server"; + +/** + * 邀请加入确认页面 + * + * 用户扫描邀请二维码后,打开此小程序页面确认加入应用 + */ + +// 微信获取手机号回调参数类型 +interface GetPhoneNumberDetail { + code?: string; + encryptedData?: string; + iv?: string; + errMsg: string; +} + +interface GetPhoneNumberEvent { + detail: GetPhoneNumberDetail; +} + +// 邀请信息类型 +interface InviteInfo { + appId: string; + appName: string; + appLogo: string; + inviterName: string; + roleName: string; +} + +// 协议类型 +type AgreementType = 'service' | 'privacy'; + +const InvitePage: React.FC = () => { + const router = useRouter(); + const [loading, setLoading] = useState(true); + const [inviteInfo, setInviteInfo] = useState(null); + const [authLoading, setAuthLoading] = useState(false); + const [agreementChecked, setAgreementChecked] = useState(false); + const [token, setToken] = useState(''); + const [error, setError] = useState(''); + + useEffect(() => { + // 从 URL 参数中获取 token + const params = router.params; + let inviteToken = params.scene || params.token || params.qrCodeKey || ''; + + // 兼容 q 参数(URL 编码的完整 URL) + if (params.q && !inviteToken) { + try { + const decodedUrl = decodeURIComponent(params.q); + const url = new URL(decodedUrl); + inviteToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || ''; + } catch (e) { + inviteToken = decodeURIComponent(params.q); + } + } + + setToken(inviteToken); + + // 获取邀请信息 + if (inviteToken) { + fetchInviteInfo(inviteToken); + } else { + setError('无效的邀请链接'); + setLoading(false); + } + }, [router.params]); + + /** + * 获取邀请信息 + */ + const fetchInviteInfo = async (inviteToken: string) => { + try { + const res = await Taro.request({ + url: `${SERVER_API_URL}/api/_app/developer/invite/info`, + method: 'GET', + data: { token: inviteToken }, + header: { 'content-type': 'application/json' } + }); + + if (res.data.code === 200 || res.data.code === 0) { + setInviteInfo(res.data.data); + } else { + setError(res.data.message || '邀请信息获取失败'); + } + } catch (err: any) { + setError(err.message || '网络请求失败'); + } finally { + setLoading(false); + } + }; + + /** + * 处理微信手机号授权 + */ + const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => { + const { code, encryptedData, iv, errMsg } = detail; + + // 检查协议是否勾选 + if (!agreementChecked) { + Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' }); + return; + } + + // 用户拒绝授权 + if (errMsg && errMsg.includes('fail')) { + Taro.showToast({ title: '需要授权手机号才能加入', icon: 'none' }); + return; + } + + if (!code) { + Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' }); + return; + } + + await handleJoinApp(code, encryptedData, iv); + }; + + /** + * 加入应用 + */ + const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => { + try { + setAuthLoading(true); + + const res = await Taro.request({ + url: `${SERVER_API_URL}/api/_app/invite/accept`, + method: 'POST', + data: { + token, + code: phoneCode, + encryptedData, + iv + }, + header: { 'content-type': 'application/json' } + }); + + if (res.data.code === 200 || res.data.code === 0) { + Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 }); + setTimeout(() => { + // 跳转到应用页面或首页 + Taro.switchTab({ url: '/pages/index/index' }); + }, 1500); + } else { + Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' }); + } + } catch (err: any) { + Taro.showToast({ title: err.message || '加入失败', icon: 'error' }); + } finally { + setAuthLoading(false); + } + }; + + /** + * 拒绝邀请 + */ + const handleReject = () => { + const pages = Taro.getCurrentPages(); + if (pages.length > 1) { + Taro.navigateBack(); + } else { + Taro.switchTab({ url: '/pages/index/index' }); + } + }; + + // 打开协议页面 + const openAgreement = (type: AgreementType) => { + const urlMap = { + service: 'https://websopy.websoft.top/agreement', + privacy: 'https://websopy.websoft.top/privacy', + }; + const targetUrl = encodeURIComponent(urlMap[type]); + Taro.navigateTo({ url: `/passport/webview/index?url=${targetUrl}` }); + }; + + // 加载中 + if (loading) { + return ( + + + + 加载中... + + + ); + } + + // 错误状态 + if (error) { + return ( + + + + {error} + + + + ); + } + + // 邀请确认页面 + return ( + + + {/* 背景效果 */} + + + {/* 渐变光晕 */} + + + {/* 主内容区域 */} + + + {/* 邀请卡片 */} + + {/* 应用信息 */} + + + {inviteInfo?.appLogo ? ( + + ) : ( + 🚀 + )} + + + {inviteInfo?.appName || 'Websopy'} + + + + 邀请你加入应用 + + + + + {/* 邀请人信息 */} + + + 邀请人:{inviteInfo?.inviterName || '某位用户'} + + + + {/* 分隔线 */} + + + {/* 协议勾选 */} + + setAgreementChecked(!agreementChecked)}> + + {agreementChecked && } + + + 我已阅读并同意 + openAgreement('service')}>《服务协议》 + + openAgreement('privacy')}>《隐私政策》 + + + {/* 主按钮 */} + + + + + {/* 拒绝按钮 */} + + 暂不加入 + + + + {/* 底部安全标识 */} + + + 安全加密连接 + + + + + ); +}; + +export default InvitePage;