feat: implement WeChat MP OAuth login

- Add wechat-mp.service.ts for MP web authorization
- Add wechat-mp.routes.ts with /api/mp endpoints
- Update EntryQRCode.vue to show H5 URL QR code
- Update HomeView.vue with WeChat auth detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-02-03 21:33:32 +08:00
parent b53e732ffa
commit 9b11f99fed
5 changed files with 415 additions and 83 deletions

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import QRCode from 'qrcode';
import { useDisplayStore } from '../stores/display';
import type { WechatLoginSuccessPayload } from '@gala/shared/types';
const props = defineProps<{
mobileUrl: string;
@@ -10,59 +8,21 @@ 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);
const qrCodeDataUrl = ref<string>('');
// 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() {
// 生成移动端H5入口二维码
async function generateQRCode() {
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(url: string) {
try {
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
// 直接使用移动端URL生成二维码
// 用户扫码后在微信内打开H5H5会自动检测微信环境并跳转授权
qrCodeDataUrl.value = await QRCode.toDataURL(props.mobileUrl, {
width: 400,
margin: 2,
color: {
@@ -70,21 +30,15 @@ async function generateQRCode(url: string) {
light: '#ffffff',
},
});
isLoading.value = false;
console.log('[EntryQRCode] QR code generated for:', props.mobileUrl);
} catch (err) {
console.error('Failed to generate QR code:', err);
console.error('[EntryQRCode] Failed to generate QR code:', err);
error.value = '生成二维码失败';
} finally {
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') {
@@ -92,30 +46,12 @@ 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(() => {
fetchWechatLoginUrl();
setupSocketListener();
generateQRCode();
window.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
cleanupSocketListener();
window.removeEventListener('keydown', handleKeydown);
});
</script>
@@ -140,7 +76,7 @@ onUnmounted(() => {
</div>
<!-- WeChat logo hint -->
<div v-if="wechatAuthUrl" class="wechat-hint">
<div 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>
@@ -155,11 +91,11 @@ onUnmounted(() => {
</div>
<div class="step">
<span class="step-num">2</span>
<span class="step-text">{{ wechatAuthUrl ? '授权登录' : '填写姓名和部门' }}</span>
<span class="step-text">确认授权登录</span>
</div>
<div class="step">
<span class="step-num">3</span>
<span class="step-text">{{ wechatAuthUrl ? '自动进入年会' : '点击进入年会' }}</span>
<span class="step-text">自动进入年会</span>
</div>
</div>