390 lines
8.9 KiB
Vue
390 lines
8.9 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue';
|
||
import { useRouter, useRoute } from 'vue-router';
|
||
import { showLoadingToast, closeToast, showToast } from 'vant';
|
||
import { useConnectionStore } from '../stores/connection';
|
||
|
||
const router = useRouter();
|
||
const route = useRoute();
|
||
const connectionStore = useConnectionStore();
|
||
|
||
const isProcessing = ref(false);
|
||
|
||
// API base URL
|
||
const apiUrl = import.meta.env.VITE_API_URL || 'http://192.168.1.5:3000';
|
||
|
||
/**
|
||
* 检测是否在微信环境中
|
||
*/
|
||
function isWechatBrowser(): boolean {
|
||
const ua = navigator.userAgent.toLowerCase();
|
||
return ua.includes('micromessenger');
|
||
}
|
||
|
||
/**
|
||
* 用code完成登录
|
||
*/
|
||
async function loginWithCode(code: string, state?: string) {
|
||
isProcessing.value = true;
|
||
showLoadingToast({ message: '登录中...', forbidClick: true });
|
||
|
||
try {
|
||
const response = await fetch(`${apiUrl}/api/mp/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ code, state }),
|
||
});
|
||
|
||
const result = await response.json();
|
||
closeToast();
|
||
|
||
if (result.success && result.data) {
|
||
// 设置用户信息
|
||
connectionStore.setUser(
|
||
result.data.userId,
|
||
result.data.userName,
|
||
result.data.sessionToken
|
||
);
|
||
|
||
showToast({ message: '登录成功!', type: 'success' });
|
||
|
||
// 跳转到投票页面
|
||
router.replace('/vote');
|
||
} else {
|
||
showToast({ message: result.error || '登录失败', type: 'fail' });
|
||
isProcessing.value = false;
|
||
}
|
||
} catch (err) {
|
||
closeToast();
|
||
showToast({ message: '网络错误,请重试', type: 'fail' });
|
||
isProcessing.value = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 跳转到微信授权页面
|
||
*/
|
||
async function redirectToWechatAuth() {
|
||
try {
|
||
// 获取授权URL
|
||
const currentUrl = window.location.href.split('?')[0]; // 移除已有的query参数
|
||
const response = await fetch(
|
||
`${apiUrl}/api/mp/auth-url?redirect_uri=${encodeURIComponent(currentUrl)}&scope=snsapi_base`
|
||
);
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.data?.authUrl) {
|
||
// 跳转到微信授权页面
|
||
window.location.href = result.data.authUrl;
|
||
} else {
|
||
console.error('[HomeView] Failed to get auth URL:', result.error);
|
||
showToast({ message: '获取授权链接失败', type: 'fail' });
|
||
}
|
||
} catch (err) {
|
||
console.error('[HomeView] Failed to redirect to auth:', err);
|
||
showToast({ message: '网络错误', type: 'fail' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理微信授权流程
|
||
*/
|
||
async function handleWechatAuth() {
|
||
// 检查URL中是否有code参数(授权回调)
|
||
const code = route.query.code as string;
|
||
const state = route.query.state as string;
|
||
|
||
if (code) {
|
||
// 有code,用code完成登录
|
||
console.log('[HomeView] Got code from callback, logging in...');
|
||
await loginWithCode(code, state);
|
||
} else {
|
||
// 无code,跳转到授权页面
|
||
console.log('[HomeView] No code, redirecting to auth...');
|
||
await redirectToWechatAuth();
|
||
}
|
||
}
|
||
|
||
// 页面加载时检查登录状态和微信环境
|
||
onMounted(async () => {
|
||
// 已登录则直接跳转
|
||
if (connectionStore.userId && connectionStore.userName && connectionStore.userName !== '访客') {
|
||
router.replace('/vote');
|
||
return;
|
||
}
|
||
|
||
// 在微信环境中自动处理授权
|
||
if (isWechatBrowser()) {
|
||
await handleWechatAuth();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="home-view safe-area-top safe-area-bottom">
|
||
<!-- Header decoration -->
|
||
<div class="header-decoration">
|
||
<div class="lantern left"></div>
|
||
<div class="lantern right"></div>
|
||
</div>
|
||
|
||
<!-- Main content -->
|
||
<div class="content">
|
||
<div class="logo-section">
|
||
<div class="year-badge">
|
||
<span class="year">2026</span>
|
||
<span class="zodiac">马年</span>
|
||
</div>
|
||
<h1 class="title gold-text">年会互动系统</h1>
|
||
<p class="subtitle">投票 · 抽奖 · 互动</p>
|
||
</div>
|
||
|
||
<div class="scan-section">
|
||
<div class="scan-hint">
|
||
<van-icon name="scan" size="64" color="#c41230" />
|
||
<p class="hint-title">请使用微信扫码登录</p>
|
||
<p class="hint-desc">请前往大屏幕扫描二维码进入年会</p>
|
||
</div>
|
||
|
||
<div class="steps">
|
||
<div class="step">
|
||
<span class="step-num">1</span>
|
||
<span class="step-text">前往大屏幕</span>
|
||
</div>
|
||
<div class="step">
|
||
<span class="step-num">2</span>
|
||
<span class="step-text">微信扫描二维码</span>
|
||
</div>
|
||
<div class="step">
|
||
<span class="step-num">3</span>
|
||
<span class="step-text">授权后自动登录</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Features -->
|
||
<div class="features">
|
||
<div class="feature-item">
|
||
<van-icon name="like-o" size="24" color="#c41230" />
|
||
<span>投票评选</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<van-icon name="gift-o" size="24" color="#d4a84b" />
|
||
<span>幸运抽奖</span>
|
||
</div>
|
||
<div class="feature-item">
|
||
<van-icon name="chart-trending-o" size="24" color="#c41230" />
|
||
<span>实时结果</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="footer">
|
||
<p>© 2026 公司年会</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
@use '../assets/styles/variables.scss' as *;
|
||
|
||
.home-view {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: linear-gradient(180deg, #fff5f5 0%, #fef8f0 50%, #ffffff 100%);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header-decoration {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 120px;
|
||
pointer-events: none;
|
||
|
||
.lantern {
|
||
position: absolute;
|
||
top: -20px;
|
||
width: 40px;
|
||
height: 60px;
|
||
background: linear-gradient(180deg, $color-primary 0%, $color-primary-dark 100%);
|
||
border-radius: 50% 50% 45% 45%;
|
||
box-shadow: 0 4px 12px rgba($color-primary, 0.3);
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -8px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 16px;
|
||
height: 12px;
|
||
background: $color-gold;
|
||
border-radius: 2px 2px 0 0;
|
||
}
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -15px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 2px;
|
||
height: 15px;
|
||
background: $color-gold;
|
||
}
|
||
|
||
&.left {
|
||
left: 20px;
|
||
animation: swing 3s ease-in-out infinite;
|
||
}
|
||
|
||
&.right {
|
||
right: 20px;
|
||
animation: swing 3s ease-in-out infinite 0.5s;
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes swing {
|
||
0%,
|
||
100% {
|
||
transform: rotate(-5deg);
|
||
}
|
||
50% {
|
||
transform: rotate(5deg);
|
||
}
|
||
}
|
||
|
||
.content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding: $spacing-xl $spacing-lg;
|
||
padding-top: 100px;
|
||
}
|
||
|
||
.logo-section {
|
||
text-align: center;
|
||
margin-bottom: $spacing-xl;
|
||
|
||
.year-badge {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
background: linear-gradient(135deg, $color-primary 0%, $color-primary-dark 100%);
|
||
color: $color-text-inverse;
|
||
padding: $spacing-sm $spacing-lg;
|
||
border-radius: $radius-lg;
|
||
margin-bottom: $spacing-md;
|
||
box-shadow: $shadow-md;
|
||
|
||
.year {
|
||
font-size: $font-size-2xl;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.zodiac {
|
||
font-size: $font-size-sm;
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
|
||
.title {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
margin-bottom: $spacing-sm;
|
||
}
|
||
|
||
.subtitle {
|
||
color: $color-text-secondary;
|
||
font-size: $font-size-md;
|
||
}
|
||
}
|
||
|
||
.scan-section {
|
||
margin-bottom: $spacing-xl;
|
||
text-align: center;
|
||
|
||
.scan-hint {
|
||
margin-bottom: $spacing-xl;
|
||
|
||
.hint-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: bold;
|
||
color: $color-text-primary;
|
||
margin: $spacing-md 0 $spacing-xs;
|
||
}
|
||
|
||
.hint-desc {
|
||
font-size: $font-size-md;
|
||
color: $color-text-secondary;
|
||
}
|
||
}
|
||
|
||
.steps {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: $spacing-lg;
|
||
padding: $spacing-md;
|
||
background: rgba($color-primary, 0.05);
|
||
border-radius: $radius-lg;
|
||
}
|
||
|
||
.step {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: $spacing-xs;
|
||
}
|
||
|
||
.step-num {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
background: $color-primary;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
font-size: $font-size-sm;
|
||
}
|
||
|
||
.step-text {
|
||
font-size: $font-size-xs;
|
||
color: $color-text-secondary;
|
||
}
|
||
}
|
||
|
||
.features {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
padding: $spacing-md 0;
|
||
|
||
.feature-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: $spacing-xs;
|
||
|
||
span {
|
||
font-size: $font-size-sm;
|
||
color: $color-text-secondary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.footer {
|
||
text-align: center;
|
||
padding: $spacing-md;
|
||
color: #999;
|
||
font-size: $font-size-xs;
|
||
}
|
||
</style>
|