Files
company-celebration/packages/client-screen/src/components/EntryQRCode.vue
empty f4736b6ebd feat: implement QR code scan login system with admin control
- Add scan login service with Redis-based token management
- Add scan login API routes for token generation and validation
- Add QRCodeLogin component for PC-side QR code display
- Add EntryQRCode component for mass login scenarios
- Add ScanLoginView for mobile-side login form
- Add localStorage persistence for user identity
- Add logout functionality to mobile client
- Add auto-redirect for logged-in users
- Add admin console control for QR code display on big screen
- Add socket event forwarding from admin to screen display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 21:44:52 +08:00

220 lines
4.3 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, onUnmounted } from 'vue';
import QRCode from 'qrcode';
const props = defineProps<{
mobileUrl: string;
}>();
const emit = defineEmits<{
close: [];
}>();
const qrCodeDataUrl = ref<string>('');
// Generate QR code
async function generateQRCode() {
try {
qrCodeDataUrl.value = await QRCode.toDataURL(props.mobileUrl, {
width: 400,
margin: 2,
color: {
dark: '#1a1a2e',
light: '#ffffff',
},
});
} catch (err) {
console.error('Failed to generate QR code:', err);
}
}
// Handle ESC key to close
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
emit('close');
}
}
onMounted(() => {
generateQRCode();
window.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
});
</script>
<template>
<div class="entry-qrcode-overlay" @click.self="emit('close')">
<div class="entry-qrcode-modal">
<!-- Close button -->
<button class="close-btn" @click="emit('close')">
<span>×</span>
</button>
<!-- Title -->
<h2 class="modal-title">扫码进入投票系统</h2>
<p class="modal-subtitle">请使用微信扫描下方二维码</p>
<!-- QR Code -->
<div class="qrcode-container">
<img v-if="qrCodeDataUrl" :src="qrCodeDataUrl" alt="入场二维码" class="qrcode-image" />
<div v-else class="qrcode-loading">生成中...</div>
</div>
<!-- URL hint -->
<p class="url-hint">{{ mobileUrl }}</p>
<!-- Instructions -->
<div class="instructions">
<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>
<!-- Footer hint -->
<p class="footer-hint"> ESC 或点击空白处关闭</p>
</div>
</div>
</template>
<style lang="scss" scoped>
@use '../assets/styles/variables.scss' as *;
.entry-qrcode-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(10px);
}
.entry-qrcode-modal {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 24px;
padding: 48px 64px;
text-align: center;
position: relative;
border: 1px solid rgba($color-gold, 0.3);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-width: 90vw;
}
.close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: none;
color: $color-text-muted;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
color: $color-text-light;
}
}
.modal-title {
font-size: 36px;
font-weight: bold;
color: $color-gold;
margin-bottom: 8px;
}
.modal-subtitle {
font-size: 18px;
color: $color-text-muted;
margin-bottom: 32px;
}
.qrcode-container {
background: white;
padding: 24px;
border-radius: 16px;
display: inline-block;
margin-bottom: 16px;
}
.qrcode-image {
width: 300px;
height: 300px;
display: block;
}
.qrcode-loading {
width: 300px;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 18px;
}
.url-hint {
font-size: 14px;
color: $color-text-muted;
margin-bottom: 32px;
font-family: monospace;
}
.instructions {
display: flex;
justify-content: center;
gap: 48px;
margin-bottom: 32px;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.step-num {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba($color-gold, 0.2);
color: $color-gold;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.step-text {
font-size: 14px;
color: $color-text-light;
}
.footer-hint {
font-size: 12px;
color: $color-text-muted;
opacity: 0.6;
}
</style>