diff --git a/.gitignore b/.gitignore index 16aaf48..4065b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ packages/server/prisma/*.db-journal # Data files 参与抽奖人员名单.xlsx 测试清单.md + +.codeartsdoer \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml index 5b56341..40a8166 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -84,6 +84,27 @@ excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: "" - +# the name by which the project can be referenced within Serena project_name: "company-celebration2" + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) included_optional_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] diff --git a/packages/client-mobile/src/components.d.ts b/packages/client-mobile/src/components.d.ts index 76e0104..de3ab23 100644 --- a/packages/client-mobile/src/components.d.ts +++ b/packages/client-mobile/src/components.d.ts @@ -12,7 +12,6 @@ declare module 'vue' { ProgramCard: typeof import('./components/ProgramCard.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - StampDock: typeof import('./components/StampDock.vue')['default'] VanButton: typeof import('vant/es')['Button'] VanCell: typeof import('vant/es')['Cell'] VanCellGroup: typeof import('vant/es')['CellGroup'] diff --git a/packages/client-mobile/src/stores/connection.ts b/packages/client-mobile/src/stores/connection.ts index 4efb60b..ba7ee9a 100644 --- a/packages/client-mobile/src/stores/connection.ts +++ b/packages/client-mobile/src/stores/connection.ts @@ -18,7 +18,7 @@ type GalaSocket = Socket; const STORAGE_KEYS = { USER_ID: 'gala_user_id', USER_NAME: 'gala_user_name', - DEPARTMENT: 'gala_department', + SESSION_TOKEN: 'gala_session_token', }; // Helper functions for localStorage @@ -50,7 +50,7 @@ export const useConnectionStore = defineStore('connection', () => { const reconnectAttempts = ref(0); const userId = ref(loadFromStorage(STORAGE_KEYS.USER_ID, null)); const userName = ref(loadFromStorage(STORAGE_KEYS.USER_NAME, null)); - const department = ref(loadFromStorage(STORAGE_KEYS.DEPARTMENT, null)); + const sessionToken = ref(loadFromStorage(STORAGE_KEYS.SESSION_TOKEN, null)); const votedCategories = ref([]); // Computed @@ -85,6 +85,7 @@ export const useConnectionStore = defineStore('connection', () => { reconnectionDelayMax: CONFIG.RECONNECTION_DELAY_MAX_MS, timeout: CONFIG.HEARTBEAT_TIMEOUT_MS, transports: ['websocket', 'polling'], + auth: sessionToken.value ? { token: sessionToken.value } : undefined, }); // Connection events @@ -193,8 +194,8 @@ export const useConnectionStore = defineStore('connection', () => { { userId: userId.value, userName: userName.value || 'Guest', - department: department.value || '未知部门', role: 'user', + sessionToken: sessionToken.value || undefined, }, (response: any) => { if (response.success) { @@ -264,15 +265,17 @@ export const useConnectionStore = defineStore('connection', () => { /** * Set user info (and persist to localStorage) */ - function setUser(id: string, name: string, dept: string) { + function setUser(id: string, name: string, token?: string) { userId.value = id; userName.value = name; - department.value = dept; + if (token) { + sessionToken.value = token; + saveToStorage(STORAGE_KEYS.SESSION_TOKEN, token); + } // 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) { @@ -287,13 +290,13 @@ export const useConnectionStore = defineStore('connection', () => { // Clear state userId.value = null; userName.value = null; - department.value = null; votedCategories.value = []; + sessionToken.value = null; // Clear localStorage localStorage.removeItem(STORAGE_KEYS.USER_ID); localStorage.removeItem(STORAGE_KEYS.USER_NAME); - localStorage.removeItem(STORAGE_KEYS.DEPARTMENT); + localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN); // Disconnect socket disconnect(); @@ -333,7 +336,7 @@ export const useConnectionStore = defineStore('connection', () => { reconnectAttempts, userId, userName, - department, + sessionToken, votedCategories, // Computed diff --git a/packages/client-mobile/src/stores/voting.ts b/packages/client-mobile/src/stores/voting.ts index ffe48c3..c5d7498 100644 --- a/packages/client-mobile/src/stores/voting.ts +++ b/packages/client-mobile/src/stores/voting.ts @@ -152,6 +152,10 @@ export const useVotingStore = defineStore('voting', () => { showToast({ message: '请先选择一个奖项', position: 'bottom' }); return false; } + if (!connectionStore.sessionToken) { + showToast({ message: '请先扫码登录', position: 'bottom' }); + return false; + } // 检查是否已为该节目投过票(任何奖项) const existingAward = getProgramAward(programId); diff --git a/packages/client-mobile/src/views/ScanLoginView.vue b/packages/client-mobile/src/views/ScanLoginView.vue index 84658c4..990a543 100644 --- a/packages/client-mobile/src/views/ScanLoginView.vue +++ b/packages/client-mobile/src/views/ScanLoginView.vue @@ -14,7 +14,6 @@ const isValidating = ref(true); const isValid = ref(false); const isSubmitting = ref(false); const userName = ref(''); -const department = ref('技术部'); const errorMessage = ref(''); // API base URL - use LAN IP for mobile access @@ -22,7 +21,7 @@ const apiUrl = import.meta.env.VITE_API_URL || 'http://192.168.1.5:3000'; // Computed const canSubmit = computed(() => { - return userName.value.trim().length > 0 && department.value.trim().length > 0; + return userName.value.trim().length > 0; }); // Validate token on mount @@ -92,7 +91,6 @@ async function handleSubmit() { body: JSON.stringify({ scanToken: token.value, userName: userName.value.trim(), - department: department.value.trim(), }), }); @@ -101,8 +99,9 @@ async function handleSubmit() { if (result.success) { // Set user info in connection store - const userId = result.data?.sessionToken || `user_${Date.now()}`; - connectionStore.setUser(userId, userName.value.trim(), department.value.trim()); + const userId = result.data?.userId || `user_${Date.now()}`; + const sessionToken = result.data?.sessionToken; + connectionStore.setUser(userId, userName.value.trim(), sessionToken); showToast({ message: '登录成功!', type: 'success' }); @@ -156,13 +155,6 @@ async function handleSubmit() { :rules="[{ required: true, message: '请输入姓名' }]" maxlength="20" /> -
diff --git a/packages/client-screen/src/router/index.ts b/packages/client-screen/src/router/index.ts index bcb019f..212a997 100644 --- a/packages/client-screen/src/router/index.ts +++ b/packages/client-screen/src/router/index.ts @@ -2,24 +2,18 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vu // Admin auth constants const ADMIN_TOKEN_KEY = 'gala_admin_token'; -const ADMIN_ACCESS_CODE = '20268888'; // Auth guard for admin routes function requireAdminAuth(to: RouteLocationNormalized) { const token = localStorage.getItem(ADMIN_TOKEN_KEY); - if (!token || token !== generateToken(ADMIN_ACCESS_CODE)) { + if (!token) { return { path: '/admin/login', query: { redirect: to.fullPath } }; } return true; } -// Simple token generator (not cryptographically secure, but sufficient for internal event) -function generateToken(code: string): string { - return btoa(`gala2026:${code}:${code.split('').reverse().join('')}`); -} - // Export for use in login component -export { ADMIN_TOKEN_KEY, ADMIN_ACCESS_CODE, generateToken }; +export { ADMIN_TOKEN_KEY }; const router = createRouter({ history: createWebHistory('/screen/'), diff --git a/packages/client-screen/src/stores/admin.ts b/packages/client-screen/src/stores/admin.ts index 23d8f5a..de05603 100644 --- a/packages/client-screen/src/stores/admin.ts +++ b/packages/client-screen/src/stores/admin.ts @@ -14,6 +14,7 @@ import type { } from '@gala/shared/types'; import { PRIZE_CONFIG } from '@gala/shared/types'; import { SOCKET_EVENTS } from '@gala/shared/constants'; +import { ADMIN_TOKEN_KEY } from '../router'; type GalaSocket = Socket; @@ -162,6 +163,8 @@ export const useAdminStore = defineStore('admin', () => { isConnecting.value = true; restoreState(); + const adminToken = localStorage.getItem(ADMIN_TOKEN_KEY) || ''; + const socketInstance = io(import.meta.env.VITE_SOCKET_URL || '', { reconnection: true, reconnectionAttempts: Infinity, @@ -169,6 +172,7 @@ export const useAdminStore = defineStore('admin', () => { reconnectionDelayMax: 5000, timeout: 10000, transports: ['websocket', 'polling'], + auth: adminToken ? { token: adminToken } : undefined, }); socketInstance.on('connect', () => { @@ -181,6 +185,7 @@ export const useAdminStore = defineStore('admin', () => { userId: 'admin_main', userName: 'Admin Console', role: 'admin', + sessionToken: adminToken || undefined, }, () => { }); // Request state sync diff --git a/packages/client-screen/src/views/AdminControl.vue b/packages/client-screen/src/views/AdminControl.vue index ac07e0d..93dc5c6 100644 --- a/packages/client-screen/src/views/AdminControl.vue +++ b/packages/client-screen/src/views/AdminControl.vue @@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'; import { useAdminStore } from '../stores/admin'; import { PRIZE_CONFIG } from '@gala/shared/types'; import type { PrizeConfig, LotteryRound } from '@gala/shared/types'; +import { ADMIN_TOKEN_KEY } from '../router'; // 简单的防抖函数 function debounce void>(fn: T, delay: number = 300): (...args: Parameters) => void { @@ -35,6 +36,15 @@ function debounceLeading void>(fn: T, delay: numbe const router = useRouter(); const admin = useAdminStore(); +const adminToken = () => localStorage.getItem(ADMIN_TOKEN_KEY) || ''; + +function getAdminHeaders(extra?: Record) { + return { + 'Content-Type': 'application/json', + 'x-session-token': adminToken(), + ...extra, + }; +} // Local UI state const confirmResetCode = ref(''); @@ -89,7 +99,9 @@ async function readJsonSafe(res: Response): Promise { async function loadPrizeConfig() { prizeConfigLoading.value = true; try { - const res = await fetch('/api/admin/prizes'); + const res = await fetch('/api/admin/prizes', { + headers: getAdminHeaders(), + }); const data = await readJsonSafe(res); if (!res.ok) { throw new Error(data?.error || data?.message || `加载奖项配置失败(${res.status})`); @@ -112,7 +124,7 @@ async function savePrizeConfig() { try { const res = await fetch('/api/admin/prizes', { method: 'PUT', - headers: { 'Content-Type': 'application/json' }, + headers: getAdminHeaders(), body: JSON.stringify({ prizes: editingPrizes.value }), }); const data = await readJsonSafe(res); @@ -172,6 +184,7 @@ async function importParticipants() { const response = await fetch('/api/admin/participants/import', { method: 'POST', + headers: { 'x-session-token': adminToken() }, body: formData, }); @@ -219,7 +232,9 @@ const tagLabels: Record = { // Load existing participants from server async function loadParticipants() { try { - const response = await fetch('/api/admin/participants'); + const response = await fetch('/api/admin/participants', { + headers: getAdminHeaders(), + }); const data = await readJsonSafe(response); if (!response.ok) { throw new Error(data?.error || data?.message || `加载参与者失败(${response.status})`); @@ -331,7 +346,7 @@ async function redrawCurrentRound() { try { const res = await fetch('/api/admin/lottery/redraw', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAdminHeaders(), }); const data = await res.json(); if (data.success) { @@ -386,7 +401,7 @@ async function confirmAdvancedCleanup() { try { const res = await fetch('/api/admin/cleanup', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: getAdminHeaders(), body: JSON.stringify({ lottery: cleanupOptions.value.lottery, voting: cleanupOptions.value.voting, diff --git a/packages/client-screen/src/views/AdminLogin.vue b/packages/client-screen/src/views/AdminLogin.vue index dfc15ef..6314823 100644 --- a/packages/client-screen/src/views/AdminLogin.vue +++ b/packages/client-screen/src/views/AdminLogin.vue @@ -1,7 +1,7 @@