feat(passport): 改造扫码登录确认页面授权登录界面及协议功能

- 重新设计授权登录页面UI,参考权大师风格,包含顶部标题、Logo与品牌名
- 添加手机号授权登录按钮及取消按钮,优化用户交互体验
- 新增服务协议和隐私政策弹窗,支持点击查看详细内容
- 增加协议勾选状态管理,未勾选协议时授权提示用户
- 修改登录确认流程,根据用户注册状态分别处理自动确认与手机号授权
- 优化按钮、状态提示和页面布局,提升整体界面一致性和可用性
This commit is contained in:
2026-04-08 02:03:10 +08:00
parent 54fa9e772c
commit 28102fb0bd
3 changed files with 288 additions and 92 deletions

View File

@@ -1,7 +1,7 @@
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 { View, Text, Button as TaroButton } from '@tarojs/components';
import { Loading, Card } from '@nutui/nutui-react-taro';
import { Success, Failure, Tips, User, Checklist } from '@nutui/icons-react-taro';
import Taro, { useRouter } from '@tarojs/taro';
import { confirmQRLogin } from '@/api/passport/qr-login';
import { loginByOpenId } from '@/api/passport/wx-login';
@@ -51,6 +51,9 @@ interface LoginResponse {
message: string;
}
// 协议弹窗类型
type AgreementType = 'service' | 'privacy' | null;
const QRConfirmPage: React.FC = () => {
const router = useRouter();
const [loading, setLoading] = useState(false);
@@ -61,6 +64,8 @@ const QRConfirmPage: React.FC = () => {
const [userInfo, setUserInfo] = useState<any>(null);
const [needAuth, setNeedAuth] = useState(false); // 是否需要手机号授权
const [authLoading, setAuthLoading] = useState(false); // 授权中状态
const [agreementChecked, setAgreementChecked] = useState(false); // 协议勾选状态
const [showAgreementPopup, setShowAgreementPopup] = useState<AgreementType>(null); // 协议弹窗
useEffect(() => {
// 从 URL 参数中获取 token
@@ -158,6 +163,15 @@ const QRConfirmPage: React.FC = () => {
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({
@@ -411,6 +425,184 @@ const QRConfirmPage: React.FC = () => {
});
};
// 打开协议弹窗
const openAgreement = (type: AgreementType) => {
setShowAgreementPopup(type);
};
// 关闭协议弹窗
const closeAgreement = () => {
setShowAgreementPopup(null);
};
// 渲染协议弹窗内容
const renderAgreementContent = () => {
if (showAgreementPopup === 'service') {
return (
<View className="p-4">
<View className="text-center mb-4">
<Text className="text-lg font-bold"></Text>
</View>
<View className="text-sm text-gray-600 leading-relaxed max-h-64 overflow-y-auto">
<Text className="block mb-2">使</Text>
<Text className="block mb-2">1. </Text>
<Text className="block mb-2">使</Text>
<Text className="block mb-2">2. </Text>
<Text className="block mb-2">使</Text>
<Text className="block mb-2">3. 使</Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">4. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">5. </Text>
<Text className="block mb-2"></Text>
</View>
<TaroButton
className="mt-4 bg-orange-500 text-white rounded-full"
onClick={closeAgreement}
>
</TaroButton>
</View>
);
}
if (showAgreementPopup === 'privacy') {
return (
<View className="p-4">
<View className="text-center mb-4">
<Text className="text-lg font-bold"></Text>
</View>
<View className="text-sm text-gray-600 leading-relaxed max-h-64 overflow-y-auto">
<Text className="block mb-2"></Text>
<Text className="block mb-2">1. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">2. 使</Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">3. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">4. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">5. </Text>
<Text className="block mb-2"></Text>
</View>
<TaroButton
className="mt-4 bg-orange-500 text-white rounded-full"
onClick={closeAgreement}
>
</TaroButton>
</View>
);
}
return null;
};
// 授权登录页面(参考权大师风格)
const renderAuthPage = () => {
return (
<View className="min-h-screen bg-white flex flex-col">
{/* 顶部导航 */}
<View className="flex items-center justify-between px-4 py-3">
<View className="w-8" />
<Text className="text-lg font-medium text-gray-800"></Text>
<View className="w-8" />
</View>
{/* Logo 区域 */}
<View className="flex-1 flex flex-col items-center justify-center px-8 -mt-20">
{/* Logo */}
<View className="w-20 h-20 mb-4">
<svg viewBox="0 0 100 100" className="w-full h-full">
<circle cx="50" cy="50" r="45" fill="#FF6B00" />
<path
d="M35 35 L50 65 L65 35"
stroke="white"
strokeWidth="6"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</View>
{/* 品牌名 */}
<Text className="text-2xl font-bold text-gray-900 mb-2"></Text>
{/* 标语 */}
<Text className="text-sm text-gray-500 tracking-widest"></Text>
</View>
{/* 底部操作区域 */}
<View className="px-6 pb-12">
{/* 主按钮 - 手机号授权登录 */}
<TaroButton
className="w-full h-12 bg-orange-500 text-white text-base font-medium rounded-lg flex items-center justify-center mb-4"
openType="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
disabled={authLoading}
style={{ backgroundColor: '#FF6B00', border: 'none' }}
>
{authLoading ? '授权中...' : '手机号授权登录'}
</TaroButton>
{/* 取消按钮 */}
<View
className="w-full h-12 flex items-center justify-center text-gray-500 text-base cursor-pointer"
onClick={handleCancel}
>
</View>
{/* 协议勾选 */}
<View className="flex items-center justify-center mt-6">
<View
className={`w-4 h-4 rounded-full border-2 flex items-center justify-center mr-2 ${
agreementChecked ? 'bg-orange-500 border-orange-500' : 'border-gray-300'
}`}
onClick={() => setAgreementChecked(!agreementChecked)}
>
{agreementChecked && (
<Checklist size="12" color="#fff" />
)}
</View>
<Text className="text-xs text-gray-500">
</Text>
<Text
className="text-xs text-orange-500 ml-1"
onClick={(e) => {
e.stopPropagation();
openAgreement('service');
}}
>
</Text>
<Text className="text-xs text-gray-500"></Text>
<Text
className="text-xs text-orange-500 ml-1"
onClick={(e) => {
e.stopPropagation();
openAgreement('privacy');
}}
>
</Text>
</View>
</View>
{/* 协议弹窗 */}
{showAgreementPopup && (
<View className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 px-6">
<View className="bg-white rounded-xl w-full max-w-sm">
{renderAgreementContent()}
</View>
</View>
)}
</View>
);
};
// 渲染状态图标
const renderStatusIcon = () => {
if (loading || authLoading) return (
@@ -437,15 +629,10 @@ const QRConfirmPage: React.FC = () => {
</View>
);
if (needAuth) return (
<View className="mb-6">
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
<svg width="32" height="32" viewBox="0 0 24 24" fill="#22c55e">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/>
</svg>
</View>
</View>
);
if (needAuth) {
// 需要授权时显示授权页面
return renderAuthPage();
}
return (
<View className="mb-6">
@@ -460,7 +647,7 @@ const QRConfirmPage: React.FC = () => {
const getTitle = () => {
if (loading || authLoading) return '正在处理...';
if (confirmed) return '登录确认成功';
if (needAuth) return '首次登录授权';
if (needAuth) return ''; // 授权页面有自己的标题
if (error) return '登录确认失败';
return loginMethod === 'url' ? '扫码登录确认' : '确认登录';
};
@@ -470,7 +657,7 @@ const QRConfirmPage: React.FC = () => {
if (loading) return '请稍候,正在为您确认登录';
if (authLoading) return '正在授权登录...';
if (confirmed) return '您已成功确认登录,网页端将自动登录';
if (needAuth) return '检测到您是首次使用,请授权手机号完成注册并登录';
if (needAuth) return ''; // 授权页面有自己的描述
if (error) return error;
if (loginMethod === 'url') {
return '检测到登录请求,是否确认登录?';
@@ -481,130 +668,93 @@ const QRConfirmPage: React.FC = () => {
// 渲染操作按钮
const renderActions = () => {
// 需要授权登录
// 需要授权登录时,按钮在授权页面内部渲染
if (needAuth) {
return (
<View className="space-y-3">
<Button
type="success"
size="large"
className="w-full rounded-xl"
open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
disabled={authLoading}
>
{authLoading ? '授权中...' : '微信手机号一键授权'}
</Button>
<Button
type="default"
size="large"
onClick={handleCancel}
className="w-full rounded-xl"
fill="none"
>
</Button>
</View>
);
return null;
}
if (loading) {
return (
<Button
type="default"
size="large"
disabled
className="w-full rounded-xl"
<View
className="w-full h-12 bg-gray-300 text-white text-base font-medium rounded-lg flex items-center justify-center"
>
...
</Button>
</View>
);
}
if (confirmed) {
return (
<Button
type="success"
size="large"
<View
className="w-full h-12 bg-green-500 text-white text-base font-medium rounded-lg flex items-center justify-center"
onClick={handleCancel}
className="w-full rounded-xl"
>
</Button>
</View>
);
}
if (error) {
return (
<View className="space-y-2">
<Button
type="primary"
size="large"
<View className="space-y-3">
<View
className="w-full h-12 bg-blue-500 text-white text-base font-medium rounded-lg flex items-center justify-center"
onClick={handleRetry}
className="w-full rounded-xl"
>
</Button>
<Button
type="default"
size="large"
</View>
<View
className="w-full h-12 bg-gray-100 text-gray-700 text-base font-medium rounded-lg flex items-center justify-center"
onClick={handleScan}
className="w-full rounded-xl"
>
</Button>
<Button
type="default"
size="small"
</View>
<View
className="w-full h-10 text-gray-500 text-sm flex items-center justify-center"
onClick={handleCancel}
className="w-full rounded-xl"
fill="none"
>
</Button>
</View>
</View>
);
}
if (loginMethod === 'scan') {
return (
<View>
<Button
type="primary"
size="large"
<View className="space-y-3">
<View
className="w-full h-12 bg-blue-500 text-white text-base font-medium rounded-lg flex items-center justify-center"
onClick={handleManualConfirm}
className="w-full mb-2 rounded-xl"
disabled={!token}
>
</Button>
<Button
type="default"
size="large"
</View>
<View
className="w-full h-12 text-gray-500 text-base flex items-center justify-center"
onClick={handleCancel}
className="w-full rounded-xl"
fill="none"
>
</Button>
</View>
</View>
);
}
return (
<Button
type="primary"
size="large"
<View
className="w-full h-12 bg-blue-500 text-white text-base font-medium rounded-lg flex items-center justify-center"
onClick={handleManualConfirm}
className="w-full rounded-xl"
>
</Button>
</View>
);
};
// 如果是授权页面,直接返回授权页面
if (needAuth) {
return renderAuthPage();
}
return (
<View className="qr-confirm-page min-h-screen bg-gradient-to-b from-blue-50 to-white">
<View className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
<View className="p-4">
{/* Logo/品牌区域 */}
<View className="text-center pt-8 pb-6">
@@ -621,14 +771,18 @@ const QRConfirmPage: React.FC = () => {
{renderStatusIcon()}
{/* 标题 */}
<Text className="text-xl font-bold text-gray-800 mb-2 block">
{getTitle()}
</Text>
{getTitle() && (
<Text className="text-xl font-bold text-gray-800 mb-2 block">
{getTitle()}
</Text>
)}
{/* 描述 */}
<Text className="text-gray-600 mb-6 block text-sm">
{getDescription()}
</Text>
{getDescription() && (
<Text className="text-gray-600 mb-6 block text-sm">
{getDescription()}
</Text>
)}
{/* 用户信息 */}
{!loading && !confirmed && !error && !needAuth && userInfo && (