feat(passport): 优化扫码登录兼容性与确认交互
- 兼容接口中 code 返回 0 和 200 两种成功状态 - 在扫码登录确认接口添加日志输出,调试响应数据 - 重构扫码登录确认页面逻辑,支持主动扫码和URL扫码两种场景 - 兼容多种token参数名,支持URL编码和旧参数解析 - URL扫码场景自动确认登录,未登录用户自动跳转登录页 - 新增主动扫码功能,支持二维码内容多格式解析(URL/JSON/纯token) - 优化确认登录后页面交互,支持自动返回或提示用户回PC端刷新 - 增加状态视觉反馈,包括加载、成功、失败及初始状态 - 优化UI细节,使用圆角样式及布局调整提升视觉体验 - 新增页面底部帮助提示文字,提升用户指引 - 新增多页面配置,设置导航栏标题及样式统一管理 - 新增应用密钥凭证、应用操作动态、应用成员、应用版本发布等增删改查功能模块及接口定义 - 新增对应页面表单组件,实现应用相关实体的新增和编辑功能
This commit is contained in:
@@ -8,37 +8,121 @@ import { useUser } from '@/hooks/useUser';
|
||||
|
||||
/**
|
||||
* 扫码登录确认页面
|
||||
* 用于处理从二维码跳转过来的登录确认
|
||||
*
|
||||
* 支持两种场景:
|
||||
* 1. 主动扫码:用户点击按钮调用微信扫码,扫描 PC 端二维码
|
||||
* 2. URL 扫码(小程序码):用户扫描小程序码后,微信自动打开此页面
|
||||
*
|
||||
* URL 扫码场景:
|
||||
* - 微信「扫普通链接二维码打开小程序」配置的二维码规则:`https://websopy.websoft.top/wx-scan/`
|
||||
* - 扫码后 URL:`https://websopy.websoft.top/wx-scan?token=xxx`
|
||||
* - 小程序接收到参数后自动确认登录
|
||||
*/
|
||||
const QRConfirmPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { user, getDisplayName } = useUser();
|
||||
const { user, getDisplayName, isLoggedIn } = useUser();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [token, setToken] = useState<string>('');
|
||||
const [loginMethod, setLoginMethod] = useState<'scan' | 'url'>('url');
|
||||
|
||||
useEffect(() => {
|
||||
// 从URL参数中获取token
|
||||
const { qrCodeKey, token: urlToken } = router.params;
|
||||
const loginToken = qrCodeKey || urlToken;
|
||||
// 从 URL 参数中获取 token
|
||||
const params = router.params;
|
||||
|
||||
// 兼容多种参数名
|
||||
// 1. 直接参数:?token=xxx
|
||||
// 2. URL 编码参数:?q=xxx(扫普通链接二维码场景)
|
||||
// 3. 旧版参数:?qrCodeKey=xxx
|
||||
let loginToken = params.token || params.qrCodeKey || '';
|
||||
|
||||
// 如果是 q 参数(URL 编码的完整 URL),需要解析
|
||||
if (params.q) {
|
||||
try {
|
||||
const decodedUrl = decodeURIComponent(params.q);
|
||||
console.log('[QRConfirm] 解码后的 URL:', decodedUrl);
|
||||
|
||||
// 解析 token
|
||||
const url = new URL(decodedUrl);
|
||||
loginToken = url.searchParams.get('token') ||
|
||||
url.searchParams.get('qrCodeKey') ||
|
||||
'';
|
||||
|
||||
setLoginMethod('url');
|
||||
} catch (e) {
|
||||
console.error('[QRConfirm] 解析 q 参数失败:', e);
|
||||
// 尝试直接使用 q 作为 token
|
||||
loginToken = decodeURIComponent(params.q);
|
||||
setLoginMethod('url');
|
||||
}
|
||||
} else if (loginToken) {
|
||||
setLoginMethod('url');
|
||||
}
|
||||
|
||||
if (loginToken) {
|
||||
setToken(loginToken);
|
||||
console.log('[QRConfirm] 获取到 token:', loginToken);
|
||||
|
||||
// URL 扫码场景:自动确认登录
|
||||
if (!params.qrCodeKey && !params.token) {
|
||||
// 如果不是直接参数,说明是 URL 扫码,自动确认
|
||||
setTimeout(() => {
|
||||
handleAutoConfirm(loginToken);
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
setError('无效的登录链接');
|
||||
}
|
||||
}, [router.params]);
|
||||
|
||||
// 确认登录
|
||||
const handleConfirmLogin = async () => {
|
||||
if (!token) {
|
||||
/**
|
||||
* 自动确认登录(URL 扫码场景)
|
||||
*/
|
||||
const handleAutoConfirm = async (loginToken: string) => {
|
||||
if (!isLoggedIn || !user?.userId) {
|
||||
// 用户未登录,跳转到登录页面
|
||||
console.log('[QRConfirm] 用户未登录,跳转到登录页');
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({
|
||||
url: '/passport/login'
|
||||
});
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleConfirmLogin(loginToken);
|
||||
};
|
||||
|
||||
/**
|
||||
* 确认登录
|
||||
*/
|
||||
const handleConfirmLogin = async (loginToken?: string) => {
|
||||
const confirmToken = loginToken || token;
|
||||
|
||||
if (!confirmToken) {
|
||||
setError('缺少登录token');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.userId) {
|
||||
setError('请先登录小程序');
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'none'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.redirectTo({
|
||||
url: '/passport/login'
|
||||
});
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +131,7 @@ const QRConfirmPage: React.FC = () => {
|
||||
setError('');
|
||||
|
||||
const result = await confirmQRLogin({
|
||||
token,
|
||||
token: confirmToken,
|
||||
userId: user.userId,
|
||||
platform: 'wechat',
|
||||
wechatInfo: {
|
||||
@@ -64,81 +148,212 @@ const QRConfirmPage: React.FC = () => {
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
// 3秒后自动返回
|
||||
// 3秒后自动关闭或返回
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
// 尝试返回上一页,如果没有则关闭
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
Taro.navigateBack();
|
||||
} else {
|
||||
// 小程序场景下,提示用户回到 PC 端
|
||||
Taro.showModal({
|
||||
title: '登录成功',
|
||||
content: '请回到电脑端刷新页面',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
setError(result.message || '登录确认失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[QRConfirm] 确认登录失败:', err);
|
||||
setError(err.message || '登录确认失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消登录
|
||||
const handleCancel = () => {
|
||||
Taro.navigateBack();
|
||||
/**
|
||||
* 手动确认登录(主动扫码场景)
|
||||
*/
|
||||
const handleManualConfirm = () => {
|
||||
handleConfirmLogin();
|
||||
};
|
||||
|
||||
// 重试
|
||||
/**
|
||||
* 取消登录
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
Taro.navigateBack();
|
||||
} else {
|
||||
Taro.switchTab({
|
||||
url: '/pages/user/user'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重试
|
||||
*/
|
||||
const handleRetry = () => {
|
||||
setError('');
|
||||
setConfirmed(false);
|
||||
handleConfirmLogin();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开微信扫码
|
||||
*/
|
||||
const handleScan = () => {
|
||||
Taro.scanCode({
|
||||
success: async (res) => {
|
||||
console.log('[QRConfirm] 扫码成功:', res);
|
||||
|
||||
// 解析二维码内容
|
||||
let scanToken = '';
|
||||
const qrContent = res.result;
|
||||
|
||||
try {
|
||||
// 尝试解析 URL
|
||||
if (qrContent.includes('http')) {
|
||||
const url = new URL(qrContent);
|
||||
scanToken = url.searchParams.get('token') ||
|
||||
url.searchParams.get('qrCodeKey') ||
|
||||
'';
|
||||
}
|
||||
|
||||
// 尝试解析 JSON
|
||||
if (!scanToken && qrContent.startsWith('{')) {
|
||||
const parsed = JSON.parse(qrContent);
|
||||
scanToken = parsed.token || parsed.qrCodeKey || '';
|
||||
}
|
||||
|
||||
// 直接作为 token
|
||||
if (!scanToken && qrContent.length >= 32) {
|
||||
scanToken = qrContent;
|
||||
}
|
||||
|
||||
if (scanToken) {
|
||||
setToken(scanToken);
|
||||
setLoginMethod('scan');
|
||||
handleConfirmLogin(scanToken);
|
||||
} else {
|
||||
setError('无效的二维码内容');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QRConfirm] 解析二维码失败:', e);
|
||||
setError('二维码解析失败');
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[QRConfirm] 扫码失败:', err);
|
||||
if (err.errMsg !== 'scanCode:fail cancel') {
|
||||
setError('扫码失败,请重试');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 渲染状态:加载中
|
||||
const renderLoading = () => (
|
||||
<View className="mb-6">
|
||||
<View className="w-16 h-16 mx-auto flex items-center justify-center">
|
||||
<Loading className="text-blue-500" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染状态:成功
|
||||
const renderSuccess = () => (
|
||||
<View className="mb-6">
|
||||
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Success className="text-green-500" size="32" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染状态:错误
|
||||
const renderError = () => (
|
||||
<View className="mb-6">
|
||||
<View className="w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
||||
<Failure className="text-red-500" size="32" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染状态:初始(用户未扫码)
|
||||
const renderInitial = () => (
|
||||
<View className="mb-6">
|
||||
<View className="w-16 h-16 mx-auto bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<User size="32" className="text-blue-500" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 获取标题
|
||||
const getTitle = () => {
|
||||
if (loading) return '正在确认登录...';
|
||||
if (confirmed) return '登录确认成功';
|
||||
if (error) return '登录确认失败';
|
||||
return loginMethod === 'url' ? '扫码登录确认' : '确认登录';
|
||||
};
|
||||
|
||||
// 获取描述
|
||||
const getDescription = () => {
|
||||
if (loading) return '请稍候,正在为您确认登录';
|
||||
if (confirmed) return '您已成功确认登录,网页端将自动登录';
|
||||
if (error) return error;
|
||||
if (loginMethod === 'url') {
|
||||
return '检测到登录请求,是否确认登录?';
|
||||
}
|
||||
return `确认使用 ${getDisplayName()} 登录网页端?`;
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="qr-confirm-page min-h-screen bg-gray-50">
|
||||
<View className="qr-confirm-page 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">
|
||||
<View className="w-20 h-20 mx-auto bg-white rounded-2xl shadow-lg flex items-center justify-center mb-4">
|
||||
<Text className="text-3xl">🔐</Text>
|
||||
</View>
|
||||
<Text className="text-gray-400 text-sm">WebSoft Platform</Text>
|
||||
</View>
|
||||
|
||||
{/* 主要内容卡片 */}
|
||||
<Card className="bg-white rounded-lg shadow-sm">
|
||||
<Card className="bg-white rounded-2xl shadow-xl -mt-4">
|
||||
<View className="p-6 text-center">
|
||||
{/* 图标 */}
|
||||
<View className="mb-6">
|
||||
{loading ? (
|
||||
<View className="w-16 h-16 mx-auto flex items-center justify-center">
|
||||
<Loading className="text-blue-500" />
|
||||
</View>
|
||||
) : confirmed ? (
|
||||
<View className="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Success className="text-green-500" />
|
||||
</View>
|
||||
) : error ? (
|
||||
<View className="w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center">
|
||||
<Failure className="text-red-500" />
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-16 h-16 mx-auto bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<User size="32" className="text-blue-500" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/* 状态图标 */}
|
||||
{loading ? renderLoading() : confirmed ? renderSuccess() : error ? renderError() : renderInitial()}
|
||||
|
||||
{/* 标题 */}
|
||||
<Text className="text-xl font-bold text-gray-800 mb-2 block">
|
||||
{loading ? '正在确认登录...' :
|
||||
confirmed ? '登录确认成功' :
|
||||
error ? '登录确认失败' : '确认登录'}
|
||||
{getTitle()}
|
||||
</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{loading ? '请稍候,正在为您确认登录' :
|
||||
confirmed ? '您已成功确认登录,网页端将自动登录' :
|
||||
error ? error :
|
||||
`确认使用 ${getDisplayName()} 登录网页端?`}
|
||||
<Text className="text-gray-600 mb-6 block text-sm">
|
||||
{getDescription()}
|
||||
</Text>
|
||||
|
||||
{/* 用户信息 */}
|
||||
{!loading && !confirmed && !error && user && (
|
||||
<View className="bg-gray-50 rounded-lg p-4 mb-6">
|
||||
<View className="bg-gray-50 rounded-xl p-4 mb-6">
|
||||
<View className="flex items-center justify-center">
|
||||
<View className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
||||
<User className="text-blue-500" size="20" />
|
||||
</View>
|
||||
{user.avatar ? (
|
||||
<View
|
||||
className="w-12 h-12 rounded-full bg-blue-100 mr-3 overflow-hidden"
|
||||
style={{ backgroundImage: `url(${user.avatar})`, backgroundSize: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<View className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3">
|
||||
<User className="text-blue-500" size="20" />
|
||||
</View>
|
||||
)}
|
||||
<View className="text-left">
|
||||
<Text className="text-sm font-medium text-gray-800 block">
|
||||
{user.nickname || user.username || '用户'}
|
||||
@@ -151,6 +366,15 @@ const QRConfirmPage: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Token 信息 */}
|
||||
{token && !loading && !confirmed && (
|
||||
<View className="bg-blue-50 rounded-lg p-3 mb-4">
|
||||
<Text className="text-xs text-blue-600">
|
||||
登录令牌:{token.substring(0, 20)}...{token.substring(token.length - 10)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="space-y-3">
|
||||
{loading ? (
|
||||
@@ -158,7 +382,7 @@ const QRConfirmPage: React.FC = () => {
|
||||
type="default"
|
||||
size="large"
|
||||
disabled
|
||||
className="w-full"
|
||||
className="w-full rounded-xl"
|
||||
>
|
||||
确认中...
|
||||
</Button>
|
||||
@@ -167,7 +391,7 @@ const QRConfirmPage: React.FC = () => {
|
||||
type="success"
|
||||
size="large"
|
||||
onClick={handleCancel}
|
||||
className="w-full"
|
||||
className="w-full rounded-xl"
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
@@ -177,27 +401,36 @@ const QRConfirmPage: React.FC = () => {
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleRetry}
|
||||
className="w-full"
|
||||
className="w-full rounded-xl"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={handleScan}
|
||||
className="w-full rounded-xl"
|
||||
>
|
||||
扫码其他二维码
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={handleCancel}
|
||||
className="w-full"
|
||||
className="w-full rounded-xl"
|
||||
fill="none"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View className="mt-3">
|
||||
) : loginMethod === 'scan' ? (
|
||||
<View>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleConfirmLogin}
|
||||
className="w-full mb-2"
|
||||
disabled={!token || !user?.userId}
|
||||
onClick={handleManualConfirm}
|
||||
className="w-full mb-2 rounded-xl"
|
||||
disabled={!token}
|
||||
>
|
||||
确认登录
|
||||
</Button>
|
||||
@@ -205,18 +438,29 @@ const QRConfirmPage: React.FC = () => {
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={handleCancel}
|
||||
className="w-full"
|
||||
className="w-full rounded-xl"
|
||||
fill="none"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
// URL 扫码场景:自动确认中
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleManualConfirm}
|
||||
className="w-full rounded-xl"
|
||||
>
|
||||
确认登录
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 安全提示 */}
|
||||
<Card className="bg-yellow-50 border border-yellow-200 rounded-lg mt-4">
|
||||
<Card className="bg-yellow-50 border border-yellow-200 rounded-xl mt-4">
|
||||
<View className="p-4">
|
||||
<View className="flex items-start">
|
||||
<Tips className="text-yellow-600 mr-2 mt-1" size="16" />
|
||||
@@ -231,6 +475,13 @@ const QRConfirmPage: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 底部说明 */}
|
||||
<View className="text-center mt-6 pb-8">
|
||||
<Text className="text-xs text-gray-400">
|
||||
如有问题,请联系管理员
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user