From 4a2f1e7c8a6c2c3ba4a04a12a49f41f451afa5d0 Mon Sep 17 00:00:00 2001 From: empty Date: Thu, 29 Jan 2026 00:28:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E5=AF=BC?= =?UTF-8?q?=E6=BC=94=E6=8E=A7=E5=88=B6=E5=8F=B0=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 高优先级优化: - 合并 REVEAL/COMPLETE 状态的重复代码模板 中优先级优化: - 添加防抖函数 (debounce/debounceLeading) 防止按钮重复点击 - 投票控制按钮添加 500ms 防抖 - 抽奖控制按钮添加 500ms 防抖 - 新增 Toast 通知系统替代 alert - 优化 API 错误反馈:loadPrizeConfig、loadParticipants、 importParticipants、savePrizeConfig 现在显示 Toast 错误提示 低优先级优化: - 新增实时奖项统计面板,显示各奖项投票 TOP3 - 添加奖项统计面板响应式样式 - 添加 Toast 动画和样式 其他: - 添加 getAwardDisplayName 辅助函数 - 优化移动端适配 --- .../client-screen/src/views/AdminControl.vue | 353 +++++++++++++++--- 1 file changed, 309 insertions(+), 44 deletions(-) diff --git a/packages/client-screen/src/views/AdminControl.vue b/packages/client-screen/src/views/AdminControl.vue index 15eb40d..b18a8dd 100644 --- a/packages/client-screen/src/views/AdminControl.vue +++ b/packages/client-screen/src/views/AdminControl.vue @@ -5,6 +5,34 @@ import { useAdminStore } from '../stores/admin'; import { PRIZE_CONFIG } from '@gala/shared/types'; import type { PrizeConfig, LotteryRound } from '@gala/shared/types'; +// 简单的防抖函数 +function debounce void>(fn: T, delay: number = 300): (...args: Parameters) => void { + let timer: ReturnType | null = null; + return (...args: Parameters) => { + if (timer) clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +} + +// 带立即执行的防抖(首次点击立即执行,后续防抖) +function debounceLeading void>(fn: T, delay: number = 300): (...args: Parameters) => void { + let timer: ReturnType | null = null; + let lastExec = 0; + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastExec > delay) { + lastExec = now; + fn(...args); + } else { + if (timer) clearTimeout(timer); + timer = setTimeout(() => { + lastExec = Date.now(); + fn(...args); + }, delay - (now - lastExec)); + } + }; +} + const router = useRouter(); const admin = useAdminStore(); @@ -13,6 +41,23 @@ const confirmResetCode = ref(''); const showResetModal = ref(false); const resetScope = ref<'all' | 'voting' | 'lottery'>('all'); +// Toast notification state +const toast = ref<{ show: boolean; message: string; type: 'error' | 'success' | 'info' }>({ + show: false, + message: '', + type: 'info', +}); +let toastTimer: ReturnType | null = null; + +// Show toast notification +function showToast(message: string, type: 'error' | 'success' | 'info' = 'info', duration: number = 3000) { + if (toastTimer) clearTimeout(toastTimer); + toast.value = { show: true, message, type }; + toastTimer = setTimeout(() => { + toast.value.show = false; + }, duration); +} + // Prize configuration state const showPrizeConfig = ref(false); const prizeConfigLoading = ref(false); @@ -44,7 +89,9 @@ async function loadPrizeConfig() { editingPrizes.value = data.data.prizes.map((p: any) => ({ ...p })); } } catch (e) { + const message = (e as Error).message; console.error('Failed to load prize config:', e); + showToast(`加载奖项配置失败: ${message}`, 'error'); } finally { prizeConfigLoading.value = false; } @@ -65,11 +112,14 @@ async function savePrizeConfig() { } if (data.success) { showPrizeConfig.value = false; + showToast('奖项配置保存成功', 'success'); } else { - alert('保存失败: ' + (data.error || '未知错误')); + const message = data.error || '未知错误'; + showToast(`保存失败: ${message}`, 'error'); } } catch (e) { - alert('保存失败: ' + (e as Error).message); + const message = (e as Error).message; + showToast(`保存失败: ${message}`, 'error'); } finally { prizeConfigSaving.value = false; } @@ -128,14 +178,22 @@ async function importParticipants() { tagDistribution: data.data?.tagDistribution ?? data.tagDistribution ?? {}, errors: data.data?.errors ?? data.errors ?? [], }; + + if (importResult.value.success) { + showToast(`成功导入 ${importResult.value.importedCount} 名参与者`, 'success'); + } else if (importResult.value.errors.length > 0) { + showToast(`导入失败: ${importResult.value.errors[0]}`, 'error'); + } } catch (error) { + const message = (error as Error).message; importResult.value = { success: false, totalCount: 0, importedCount: 0, tagDistribution: {}, - errors: [(error as Error).message], + errors: [message], }; + showToast(`导入失败: ${message}`, 'error'); } finally { importing.value = false; } @@ -167,7 +225,9 @@ async function loadParticipants() { }; } } catch (error) { + const message = (error as Error).message; console.error('Failed to load participants:', error); + showToast(`加载参与者失败: ${message}`, 'error'); } } @@ -181,22 +241,22 @@ function setPhase(phase: 'IDLE' | 'VOTING' | 'LOTTERY' | 'RESULTS') { admin.setPhase(phase); } -// Voting control -function openVoting() { +// Voting control with debounce +const openVoting = debounceLeading(() => { admin.controlVoting('open'); -} +}, 500); -function closeVoting() { +const closeVoting = debounceLeading(() => { admin.controlVoting('close'); -} +}, 500); -function pauseVoting() { +const pauseVoting = debounceLeading(() => { admin.controlVoting('pause'); -} +}, 500); -function resumeVoting() { +const resumeVoting = debounceLeading(() => { admin.controlVoting('resume'); -} +}, 500); // Program voting control function nextProgram() { @@ -233,27 +293,27 @@ function toggleLateCatch() { } } -// Lottery control -function startGalaxy() { +// Lottery control with debounce +const startGalaxy = debounceLeading(() => { admin.controlLottery('start_galaxy'); -} +}, 500); -function startStorm() { +const startStorm = debounceLeading(() => { admin.controlLottery('start_storm'); -} +}, 500); -function stopAndReveal() { +const stopAndReveal = debounceLeading(() => { if (!admin.canStopLottery) return; admin.controlLottery('stop_reveal'); -} +}, 500); -function nextRound() { +const nextRound = debounceLeading(() => { admin.controlLottery('next_round'); -} +}, 500); -function resetLottery() { +const resetLottery = debounceLeading(() => { admin.controlLottery('reset'); -} +}, 500); // Emergency reset function openResetModal(scope: 'all' | 'voting' | 'lottery') { @@ -334,6 +394,12 @@ const lotteryPhaseLabel = computed(() => { } }); +// Get award display name from awards config +function getAwardDisplayName(awardId: string): string { + const award = admin.awards.find((a: any) => a.id === awardId); + return award?.name || awardId; +} + // Compute award statistics grouped by award type const awardStats = computed(() => { const stats: Record> = {}; @@ -413,6 +479,17 @@ onMounted(() => { × + + +
+ + {{ toast.type === 'success' ? '✓' : toast.type === 'error' ? '✕' : 'ℹ' }} + + {{ toast.message }} + × +
+
+
@@ -552,26 +629,8 @@ onMounted(() => { 停止抽奖 - -
- - -
- - -
+ +
+ +
+
+

📊 实时奖项统计

+
+
+
+ 暂无投票数据 +
+
+
+
+ {{ getAwardDisplayName(awardId) }} + {{ programs.reduce((sum, p) => sum + p.votes, 0) }} 票 +
+
+
+ {{ index + 1 }} + {{ program.programName }} + {{ program.votes }} +
+
+{{ programs.length - 3 }} 个节目
+
+
+
+
+
+
@@ -937,6 +1024,70 @@ $admin-danger: #ef4444; to { transform: translateX(-50%) translateY(0); opacity: 1; } } +// Toast Notification +.toast-notification { + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 300; + cursor: pointer; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + min-width: 280px; + max-width: 90vw; + + &.success { + background: rgba(34, 197, 94, 0.95); + color: white; + } + + &.error { + background: rgba(239, 68, 68, 0.95); + color: white; + } + + &.info { + background: rgba(59, 130, 246, 0.95); + color: white; + } + + .toast-icon { + font-size: 18px; + font-weight: bold; + } + + .toast-message { + flex: 1; + } + + .toast-close { + font-size: 20px; + opacity: 0.7; + margin-left: 8px; + } +} + +.toast-enter-active, +.toast-leave-active { + transition: all 0.3s ease; +} + +.toast-enter-from { + transform: translateX(-50%) translateY(-20px); + opacity: 0; +} + +.toast-leave-to { + transform: translateX(-50%) translateY(-20px); + opacity: 0; +} + // Control Sections .control-section { background: $admin-surface; @@ -1007,6 +1158,106 @@ $admin-danger: #ef4444; padding: 20px; } +// Stats Section +.stats-section { + grid-column: span 2; +} + +.no-stats { + text-align: center; + color: #666; + padding: 40px; + font-style: italic; +} + +.award-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.award-stat-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid $admin-border; + border-radius: 8px; + overflow: hidden; +} + +.award-stat-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid $admin-border; + + .award-stat-name { + font-size: 14px; + font-weight: 600; + color: $color-gold; + } + + .award-stat-total { + font-size: 12px; + color: $admin-success; + background: rgba($admin-success, 0.15); + padding: 4px 10px; + border-radius: 4px; + } +} + +.award-stat-programs { + padding: 8px; +} + +.award-stat-item { + display: grid; + grid-template-columns: 24px 1fr auto; + gap: 8px; + align-items: center; + padding: 8px 10px; + border-radius: 4px; + font-size: 13px; + + &:hover { + background: rgba(255, 255, 255, 0.03); + } + + .stat-rank { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: rgba($color-gold, 0.2); + color: $color-gold; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + } + + .stat-name { + color: $admin-text; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .stat-votes { + color: $admin-success; + font-weight: 600; + font-variant-numeric: tabular-nums; + } +} + +.stat-more { + text-align: center; + padding: 8px; + font-size: 12px; + color: #666; + font-style: italic; +} + // Stats .stat-row { display: flex; @@ -1495,11 +1746,25 @@ $admin-danger: #ef4444; // Control Sections .control-section { &.lottery-section, - &.global-section { + &.global-section, + &.stats-section { grid-column: span 1; } } + // Stats Section Mobile + .award-stats-grid { + grid-template-columns: 1fr; + } + + .award-stat-header { + padding: 10px 12px; + + .award-stat-name { + font-size: 13px; + } + } + .section-header { flex-wrap: wrap; gap: 8px;