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>
This commit is contained in:
empty
2026-01-25 21:44:52 +08:00
parent 75570af8bc
commit f4736b6ebd
26 changed files with 1925 additions and 119 deletions

View File

@@ -14,6 +14,31 @@ import { CONFIG } from '@gala/shared/constants';
type GalaSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
// LocalStorage keys
const STORAGE_KEYS = {
USER_ID: 'gala_user_id',
USER_NAME: 'gala_user_name',
DEPARTMENT: 'gala_department',
};
// Helper functions for localStorage
function loadFromStorage<T>(key: string, defaultValue: T): T {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
} catch {
return defaultValue;
}
}
function saveToStorage(key: string, value: unknown): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.warn('[Storage] Failed to save:', e);
}
}
export const useConnectionStore = defineStore('connection', () => {
// State - use shallowRef for socket to avoid deep reactivity issues
const socket = shallowRef<GalaSocket | null>(null);
@@ -23,9 +48,9 @@ export const useConnectionStore = defineStore('connection', () => {
const lastPingTime = ref<number>(0);
const latency = ref<number>(0);
const reconnectAttempts = ref(0);
const userId = ref<string | null>(null);
const userName = ref<string | null>(null);
const department = ref<string | null>(null);
const userId = ref<string | null>(loadFromStorage(STORAGE_KEYS.USER_ID, null));
const userName = ref<string | null>(loadFromStorage(STORAGE_KEYS.USER_NAME, null));
const department = ref<string | null>(loadFromStorage(STORAGE_KEYS.DEPARTMENT, null));
const votedCategories = ref<VoteCategory[]>([]);
// Computed
@@ -69,10 +94,12 @@ export const useConnectionStore = defineStore('connection', () => {
isConnecting.value = false;
reconnectAttempts.value = 0;
// Auto-generate userId if not set
// Auto-generate userId if not set (and save to localStorage)
if (!userId.value) {
userId.value = `user_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
userName.value = '访客';
saveToStorage(STORAGE_KEYS.USER_ID, userId.value);
saveToStorage(STORAGE_KEYS.USER_NAME, userName.value);
}
// Join with user info
@@ -217,19 +244,43 @@ export const useConnectionStore = defineStore('connection', () => {
}
/**
* Set user info
* Set user info (and persist to localStorage)
*/
function setUser(id: string, name: string, dept: string) {
userId.value = id;
userName.value = name;
department.value = dept;
// Persist to localStorage
saveToStorage(STORAGE_KEYS.USER_ID, id);
saveToStorage(STORAGE_KEYS.USER_NAME, name);
saveToStorage(STORAGE_KEYS.DEPARTMENT, dept);
// Rejoin if already connected
if (socket.value?.connected) {
joinRoom();
}
}
/**
* Logout and clear stored user info
*/
function logout() {
// Clear state
userId.value = null;
userName.value = null;
department.value = null;
votedCategories.value = [];
// Clear localStorage
localStorage.removeItem(STORAGE_KEYS.USER_ID);
localStorage.removeItem(STORAGE_KEYS.USER_NAME);
localStorage.removeItem(STORAGE_KEYS.DEPARTMENT);
// Disconnect socket
disconnect();
}
/**
* Add voted category
*/
@@ -274,6 +325,7 @@ export const useConnectionStore = defineStore('connection', () => {
// Actions
connect,
disconnect,
logout,
setUser,
addVotedCategory,
requestSync,