feat(passport): 改造扫码登录确认页面授权登录界面及协议功能
- 重新设计授权登录页面UI,参考权大师风格,包含顶部标题、Logo与品牌名 - 添加手机号授权登录按钮及取消按钮,优化用户交互体验 - 新增服务协议和隐私政策弹窗,支持点击查看详细内容 - 增加协议勾选状态管理,未勾选协议时授权提示用户 - 修改登录确认流程,根据用户注册状态分别处理自动确认与手机号授权 - 优化按钮、状态提示和页面布局,提升整体界面一致性和可用性
This commit is contained in:
@@ -13,5 +13,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1775583343042
|
||||
"lastUpdated": 1775584636978
|
||||
}
|
||||
42
.workbuddy/memory/2026-04-08.md
Normal file
42
.workbuddy/memory/2026-04-08.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 2026-04-08 工作记录
|
||||
|
||||
## passport/qr-confirm/index.tsx 页面改造
|
||||
|
||||
### 改造内容
|
||||
将扫码登录确认页面的授权登录界面改造成参考权大师的设计风格:
|
||||
|
||||
1. **UI 改造**
|
||||
- 顶部标题"请登录"
|
||||
- 中间 Logo + 品牌名 + 标语(权大师风格)
|
||||
- 橙色主按钮"手机号授权登录"
|
||||
- 灰色"取消"按钮
|
||||
- 底部协议勾选(带链接可点击查看详情)
|
||||
|
||||
2. **新增功能**
|
||||
- 协议勾选状态管理
|
||||
- 服务协议和隐私政策弹窗
|
||||
- 未勾选协议时点击授权按钮会提示
|
||||
|
||||
3. **保留功能**
|
||||
- 已注册用户的自动登录确认流程
|
||||
- 未注册用户的手机号授权登录
|
||||
- 扫码登录成功后的跳转逻辑
|
||||
|
||||
### 页面流程
|
||||
```
|
||||
用户扫码 → qr-confirm 页面
|
||||
↓
|
||||
检测用户状态
|
||||
↓
|
||||
┌──────────────┬──────────────┐
|
||||
已注册 未注册
|
||||
↓ ↓
|
||||
自动确认登录 显示授权登录界面
|
||||
↓ ↓
|
||||
登录成功 一键授权登录
|
||||
```
|
||||
|
||||
### 技术要点
|
||||
- 使用 Taro 的 Button 组件 openType="getPhoneNumber" 获取微信手机号授权
|
||||
- 授权前校验协议勾选状态
|
||||
- 调用后端 loginByMpWxPhone 接口(带 notVerifyPhone: true 自动注册)
|
||||
@@ -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()}
|
||||
|
||||
{/* 标题 */}
|
||||
{getTitle() && (
|
||||
<Text className="text-xl font-bold text-gray-800 mb-2 block">
|
||||
{getTitle()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
{getDescription() && (
|
||||
<Text className="text-gray-600 mb-6 block text-sm">
|
||||
{getDescription()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 用户信息 */}
|
||||
{!loading && !confirmed && !error && !needAuth && userInfo && (
|
||||
|
||||
Reference in New Issue
Block a user