Files
company-celebration/packages/client-mobile/src/views/HomeView.vue
2026-02-04 01:29:05 +08:00

390 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>