feat: integrate WeChat Open Platform QR code login
- Add WeChat service for OAuth2 authentication flow - Add WeChat routes (/api/wechat/login, /api/wechat/callback) - Add WeChat types for login state and responses - Update EntryQRCode component to support WeChat login - Add WeChat config options (appId, appSecret, redirectUri) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import QRCode from 'qrcode';
|
||||
import { useDisplayStore } from '../stores/display';
|
||||
import type { WechatLoginSuccessPayload } from '@gala/shared/types';
|
||||
|
||||
const props = defineProps<{
|
||||
mobileUrl: string;
|
||||
@@ -8,14 +10,59 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
loginSuccess: [payload: WechatLoginSuccessPayload];
|
||||
}>();
|
||||
|
||||
const displayStore = useDisplayStore();
|
||||
|
||||
const qrCodeDataUrl = ref<string>('');
|
||||
const isLoading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
const wechatAuthUrl = ref<string | null>(null);
|
||||
const loginState = ref<string | null>(null);
|
||||
|
||||
// Use WeChat auth URL if available, otherwise fallback to mobile URL
|
||||
const qrUrl = computed(() => wechatAuthUrl.value || props.mobileUrl);
|
||||
|
||||
// Fetch WeChat login URL from server
|
||||
async function fetchWechatLoginUrl() {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const socket = displayStore.getSocket();
|
||||
const socketId = socket?.id;
|
||||
|
||||
if (!socketId) {
|
||||
console.warn('[EntryQRCode] Socket not connected, using fallback URL');
|
||||
await generateQRCode(props.mobileUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = import.meta.env.VITE_API_URL || '';
|
||||
const response = await fetch(`${apiUrl}/api/wechat/login?pcSocketId=${socketId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
wechatAuthUrl.value = result.data.authUrl;
|
||||
loginState.value = result.data.state;
|
||||
await generateQRCode(result.data.authUrl);
|
||||
console.log('[EntryQRCode] WeChat auth URL obtained');
|
||||
} else {
|
||||
console.warn('[EntryQRCode] WeChat not configured, using fallback URL');
|
||||
await generateQRCode(props.mobileUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[EntryQRCode] Failed to fetch WeChat login URL:', err);
|
||||
// Fallback to mobile URL
|
||||
await generateQRCode(props.mobileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate QR code
|
||||
async function generateQRCode() {
|
||||
async function generateQRCode(url: string) {
|
||||
try {
|
||||
qrCodeDataUrl.value = await QRCode.toDataURL(props.mobileUrl, {
|
||||
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
|
||||
width: 400,
|
||||
margin: 2,
|
||||
color: {
|
||||
@@ -23,11 +70,21 @@ async function generateQRCode() {
|
||||
light: '#ffffff',
|
||||
},
|
||||
});
|
||||
isLoading.value = false;
|
||||
} catch (err) {
|
||||
console.error('Failed to generate QR code:', err);
|
||||
error.value = '生成二维码失败';
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WeChat login success event
|
||||
function handleWechatLoginSuccess(payload: WechatLoginSuccessPayload) {
|
||||
console.log('[EntryQRCode] WeChat login success:', payload);
|
||||
emit('loginSuccess', payload);
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// Handle ESC key to close
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -35,12 +92,30 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Setup WebSocket listener for login success
|
||||
function setupSocketListener() {
|
||||
const socket = displayStore.getSocket();
|
||||
if (socket) {
|
||||
socket.on('wechat:login_success' as any, handleWechatLoginSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup WebSocket listener
|
||||
function cleanupSocketListener() {
|
||||
const socket = displayStore.getSocket();
|
||||
if (socket) {
|
||||
socket.off('wechat:login_success' as any, handleWechatLoginSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
generateQRCode();
|
||||
fetchWechatLoginUrl();
|
||||
setupSocketListener();
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupSocketListener();
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
@@ -59,12 +134,18 @@ onUnmounted(() => {
|
||||
|
||||
<!-- QR Code -->
|
||||
<div class="qrcode-container">
|
||||
<img v-if="qrCodeDataUrl" :src="qrCodeDataUrl" alt="入场二维码" class="qrcode-image" />
|
||||
<div v-else class="qrcode-loading">生成中...</div>
|
||||
<div v-if="isLoading" class="qrcode-loading">生成中...</div>
|
||||
<div v-else-if="error" class="qrcode-error">{{ error }}</div>
|
||||
<img v-else :src="qrCodeDataUrl" alt="入场二维码" class="qrcode-image" />
|
||||
</div>
|
||||
|
||||
<!-- URL hint -->
|
||||
<p class="url-hint">{{ mobileUrl }}</p>
|
||||
<!-- WeChat logo hint -->
|
||||
<div v-if="wechatAuthUrl" class="wechat-hint">
|
||||
<svg class="wechat-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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-.406-.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>
|
||||
<span>微信扫码登录</span>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="instructions">
|
||||
@@ -74,11 +155,11 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">2</span>
|
||||
<span class="step-text">填写姓名和部门</span>
|
||||
<span class="step-text">{{ wechatAuthUrl ? '授权登录' : '填写姓名和部门' }}</span>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">3</span>
|
||||
<span class="step-text">点击进入年会</span>
|
||||
<span class="step-text">{{ wechatAuthUrl ? '自动进入年会' : '点击进入年会' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +244,8 @@ onUnmounted(() => {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.qrcode-loading {
|
||||
.qrcode-loading,
|
||||
.qrcode-error {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
@@ -173,11 +255,23 @@ onUnmounted(() => {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.url-hint {
|
||||
font-size: 14px;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 32px;
|
||||
font-family: monospace;
|
||||
.qrcode-error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.wechat-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #07c160;
|
||||
font-size: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
|
||||
Reference in New Issue
Block a user