From 86f972d694c68612c891b24ff22b57f16a441778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 11 Apr 2026 16:01:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(invite):=20=E5=A2=9E=E5=8A=A0=E9=82=80?= =?UTF-8?q?=E8=AF=B7=E5=8A=A0=E5=85=A5=E7=A1=AE=E8=AE=A4=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 app.config.ts 中添加 invite 模块路径 - 在 project.private.config.json 中配置小程序的邀请确认页面入口 - 新增邀请加入页面配置文件,设置页面标题和样式 - 实现邀请加入页面核心逻辑,支持获取邀请信息和微信手机号授权 - 支持用户同意协议后快速加入应用,加入成功后跳转首页 - 实现拒绝加入功能,支持返回上一页或首页 - 页面UI设计带渐变背景、邀请信息展示和协议勾选交互 - 提供服务协议和隐私政策跳转查看链接 - 添加加载和错误状态的用户友好提示界面 --- project.private.config.json | 21 +- src/app.config.ts | 1 + src/passport/invite/index.config.ts | 5 + src/passport/invite/index.tsx | 338 ++++++++++++++++++++++++++++ 4 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 src/passport/invite/index.config.ts create mode 100644 src/passport/invite/index.tsx 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;