- Add PostcardItem.vue component with Chinese postal aesthetics - Add PostcardGrid.vue container with 4x2 CSS Grid layout - Add Postmark.vue component for real-time vote stamp visualization - Update LiveVotingView.vue with cream paper theme (#FDFBF7) - Add Year of the Horse 2026 stamp image - Add responsive breakpoints for different screen sizes - Enhance admin service with program voting control - Add vote stamp accumulation for big screen display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1067 lines
24 KiB
Vue
1067 lines
24 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useAdminStore } from '../stores/admin';
|
||
import { PRIZE_CONFIG } from '@gala/shared/types';
|
||
|
||
const router = useRouter();
|
||
const admin = useAdminStore();
|
||
|
||
// Local UI state
|
||
const confirmResetCode = ref('');
|
||
const showResetModal = ref(false);
|
||
const resetScope = ref<'all' | 'voting' | 'lottery'>('all');
|
||
|
||
// Countdown timer for stop button
|
||
const countdownDisplay = ref(3);
|
||
let countdownInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
||
// Navigation
|
||
function goBack() {
|
||
router.push('/');
|
||
}
|
||
|
||
// Phase control
|
||
function setPhase(phase: 'IDLE' | 'VOTING' | 'LOTTERY' | 'RESULTS') {
|
||
admin.setPhase(phase);
|
||
}
|
||
|
||
// Voting control
|
||
function openVoting() {
|
||
admin.controlVoting('open');
|
||
}
|
||
|
||
function closeVoting() {
|
||
admin.controlVoting('close');
|
||
}
|
||
|
||
function pauseVoting() {
|
||
admin.controlVoting('pause');
|
||
}
|
||
|
||
function resumeVoting() {
|
||
admin.controlVoting('resume');
|
||
}
|
||
|
||
// Program voting control
|
||
function nextProgram() {
|
||
const socket = admin.getSocket();
|
||
if (socket) {
|
||
socket.emit('admin:next_program' as any, {}, (response: any) => {
|
||
if (!response.success) {
|
||
console.error('Failed to move to next program:', response.message);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function startProgramVoting(programId: string) {
|
||
const socket = admin.getSocket();
|
||
if (socket) {
|
||
socket.emit('admin:start_program' as any, { programId }, (response: any) => {
|
||
if (!response.success) {
|
||
console.error('Failed to start program voting:', response.message);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function toggleLateCatch() {
|
||
const socket = admin.getSocket();
|
||
if (socket) {
|
||
const newValue = !admin.allowLateCatch;
|
||
socket.emit('admin:toggle_late_catch' as any, { enabled: newValue }, (response: any) => {
|
||
if (!response.success) {
|
||
console.error('Failed to toggle late catch:', response.message);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Lottery control
|
||
function startGalaxy() {
|
||
admin.controlLottery('start_galaxy');
|
||
}
|
||
|
||
function startStorm() {
|
||
admin.controlLottery('start_storm');
|
||
// Start countdown
|
||
countdownDisplay.value = 3;
|
||
countdownInterval = setInterval(() => {
|
||
countdownDisplay.value = Math.max(0, countdownDisplay.value - 1);
|
||
if (countdownDisplay.value === 0 && countdownInterval) {
|
||
clearInterval(countdownInterval);
|
||
countdownInterval = null;
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function stopAndReveal() {
|
||
if (!admin.canStopLottery) return;
|
||
admin.controlLottery('stop_reveal');
|
||
}
|
||
|
||
function nextRound() {
|
||
admin.controlLottery('next_round');
|
||
}
|
||
|
||
function resetLottery() {
|
||
admin.controlLottery('reset');
|
||
}
|
||
|
||
// Emergency reset
|
||
function openResetModal(scope: 'all' | 'voting' | 'lottery') {
|
||
resetScope.value = scope;
|
||
confirmResetCode.value = '';
|
||
showResetModal.value = true;
|
||
}
|
||
|
||
function confirmReset() {
|
||
admin.emergencyReset(resetScope.value, confirmResetCode.value);
|
||
showResetModal.value = false;
|
||
confirmResetCode.value = '';
|
||
}
|
||
|
||
function cancelReset() {
|
||
showResetModal.value = false;
|
||
confirmResetCode.value = '';
|
||
}
|
||
|
||
// Music control
|
||
function toggleMusic() {
|
||
if (admin.musicPlaying) {
|
||
admin.controlMusic('stop');
|
||
} else {
|
||
admin.controlMusic('play', 'bgm');
|
||
}
|
||
}
|
||
|
||
function playFanfare() {
|
||
admin.controlMusic('play', 'fanfare');
|
||
}
|
||
|
||
// Computed helpers
|
||
const currentPrizeConfig = computed(() => {
|
||
return PRIZE_CONFIG.find(p => p.round === admin.lotteryRound) || PRIZE_CONFIG[0];
|
||
});
|
||
|
||
const isLastRound = computed(() => admin.lotteryRound >= 4);
|
||
|
||
const phaseLabel = computed(() => {
|
||
switch (admin.systemPhase) {
|
||
case 'IDLE': return '待机';
|
||
case 'VOTING': return '投票中';
|
||
case 'LOTTERY': return '抽奖中';
|
||
case 'RESULTS': return '结果展示';
|
||
default: return '未知';
|
||
}
|
||
});
|
||
|
||
const lotteryPhaseLabel = computed(() => {
|
||
switch (admin.lotterySubPhase) {
|
||
case 'IDLE': return '准备就绪';
|
||
case 'GALAXY': return '展示参与者';
|
||
case 'STORM': return '滚动中...';
|
||
case 'REVEAL': return '揭晓中';
|
||
case 'COMPLETE': return '已完成';
|
||
default: return '未知';
|
||
}
|
||
});
|
||
|
||
// Lifecycle
|
||
onMounted(() => {
|
||
admin.connect();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (countdownInterval) {
|
||
clearInterval(countdownInterval);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="admin-control">
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<button class="back-btn" @click="goBack">← 返回</button>
|
||
<h1 class="title">管理控制台</h1>
|
||
<div class="status-bar">
|
||
<span class="status-item">
|
||
<span class="dot" :class="admin.isConnected ? 'online' : 'offline'"></span>
|
||
{{ admin.isConnected ? '已连接' : '未连接' }}
|
||
</span>
|
||
<span class="status-item">
|
||
<span class="icon">👥</span>
|
||
{{ admin.onlineUsers }} 人在线
|
||
</span>
|
||
<span class="status-item phase-badge" :class="admin.systemPhase.toLowerCase()">
|
||
{{ phaseLabel }}
|
||
</span>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Error Toast -->
|
||
<div v-if="admin.lastError" class="error-toast" @click="admin.clearError">
|
||
{{ admin.lastError }}
|
||
<span class="close">×</span>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<main class="content">
|
||
<!-- Section A: Voting Control -->
|
||
<section class="control-section">
|
||
<div class="section-header">
|
||
<h2>投票控制</h2>
|
||
<span class="section-status" :class="{ active: admin.votingOpen }">
|
||
{{ admin.votingOpen ? (admin.votingPaused ? '已暂停' : '进行中') : '已关闭' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="section-body">
|
||
<div class="stat-row">
|
||
<div class="stat-item">
|
||
<span class="stat-label">总投票数</span>
|
||
<span class="stat-value">{{ admin.totalVotes }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button
|
||
class="ctrl-btn primary"
|
||
:disabled="admin.votingOpen || admin.votingPaused || admin.pendingAction === 'voting_open'"
|
||
@click="openVoting"
|
||
>
|
||
开启投票
|
||
</button>
|
||
<button
|
||
class="ctrl-btn warning"
|
||
:disabled="!admin.votingOpen || admin.pendingAction === 'voting_pause'"
|
||
@click="pauseVoting"
|
||
>
|
||
暂停
|
||
</button>
|
||
<button
|
||
class="ctrl-btn success"
|
||
:disabled="!admin.votingPaused || admin.pendingAction === 'voting_resume'"
|
||
@click="resumeVoting"
|
||
>
|
||
恢复
|
||
</button>
|
||
<button
|
||
class="ctrl-btn danger"
|
||
:disabled="(!admin.votingOpen && !admin.votingPaused) || admin.pendingAction === 'voting_close'"
|
||
@click="closeVoting"
|
||
>
|
||
关闭投票
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section A2: Program Votes Display -->
|
||
<section class="control-section program-section">
|
||
<div class="section-header">
|
||
<h2>节目票数</h2>
|
||
<span class="section-status">共 {{ admin.programs.length }} 个节目</span>
|
||
</div>
|
||
|
||
<div class="section-body">
|
||
<!-- Program Vote List (Read-only) -->
|
||
<div class="program-list">
|
||
<div
|
||
v-for="(program, idx) in admin.programs"
|
||
:key="program.id"
|
||
class="program-item readonly"
|
||
>
|
||
<span class="program-order">{{ idx + 1 }}</span>
|
||
<span class="program-name">{{ program.name }}</span>
|
||
<span class="program-team">{{ program.teamName }}</span>
|
||
<span class="program-votes">{{ program.votes }} 票</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section B: Lottery Controller -->
|
||
<section class="control-section lottery-section">
|
||
<div class="section-header">
|
||
<h2>抽奖控制</h2>
|
||
<div class="round-indicator">
|
||
<span class="round-badge">第 {{ admin.lotteryRound }} 轮</span>
|
||
<span class="phase-badge">{{ lotteryPhaseLabel }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-body">
|
||
<!-- Prize Info -->
|
||
<div class="prize-info">
|
||
<div class="prize-level">{{ currentPrizeConfig.level }}</div>
|
||
<div class="prize-name">{{ currentPrizeConfig.name }}</div>
|
||
<div class="prize-count">× {{ currentPrizeConfig.winnerCount }} 名</div>
|
||
<div v-if="currentPrizeConfig.zodiacFilter" class="prize-filter">
|
||
限定: 属马
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lottery State Machine Controls -->
|
||
<div class="lottery-controls">
|
||
<!-- IDLE State -->
|
||
<button
|
||
v-if="admin.lotterySubPhase === 'IDLE'"
|
||
class="ctrl-btn primary large"
|
||
:disabled="admin.pendingAction === 'lottery_start_galaxy'"
|
||
@click="startGalaxy"
|
||
>
|
||
开始展示
|
||
</button>
|
||
|
||
<!-- GALAXY State -->
|
||
<button
|
||
v-if="admin.lotterySubPhase === 'GALAXY'"
|
||
class="ctrl-btn warning large"
|
||
:disabled="admin.pendingAction === 'lottery_start_storm'"
|
||
@click="startStorm"
|
||
>
|
||
开始滚动
|
||
</button>
|
||
|
||
<!-- STORM State -->
|
||
<button
|
||
v-if="admin.lotterySubPhase === 'STORM'"
|
||
class="ctrl-btn danger large"
|
||
:disabled="!admin.canStopLottery"
|
||
@click="stopAndReveal"
|
||
>
|
||
<span v-if="!admin.canStopLottery" class="countdown">
|
||
{{ countdownDisplay }}s
|
||
</span>
|
||
<span v-else>停止抽奖</span>
|
||
</button>
|
||
|
||
<!-- COMPLETE State -->
|
||
<div v-if="admin.lotterySubPhase === 'COMPLETE'" class="complete-controls">
|
||
<button
|
||
class="ctrl-btn primary"
|
||
:disabled="isLastRound || admin.pendingAction === 'lottery_next_round'"
|
||
@click="nextRound"
|
||
>
|
||
下一轮
|
||
</button>
|
||
<button
|
||
class="ctrl-btn outline"
|
||
:disabled="admin.pendingAction === 'lottery_reset'"
|
||
@click="resetLottery"
|
||
>
|
||
重置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Winners Display -->
|
||
<div v-if="admin.currentWinners.length > 0" class="winners-display">
|
||
<h4>本轮中奖者</h4>
|
||
<div class="winners-list">
|
||
<div v-for="winner in admin.currentWinners" :key="winner.id" class="winner-item">
|
||
<span class="winner-name">{{ winner.name }}</span>
|
||
<span class="winner-dept">{{ winner.department }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Round Progress -->
|
||
<div class="round-progress">
|
||
<div
|
||
v-for="round in 4"
|
||
:key="round"
|
||
class="round-dot"
|
||
:class="{
|
||
active: round === admin.lotteryRound,
|
||
completed: round < admin.lotteryRound
|
||
}"
|
||
>
|
||
{{ round }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Section C: Global Controls -->
|
||
<section class="control-section global-section">
|
||
<div class="section-header">
|
||
<h2>全局控制</h2>
|
||
</div>
|
||
|
||
<div class="section-body">
|
||
<!-- Display Mode -->
|
||
<div class="control-group">
|
||
<h4>显示模式</h4>
|
||
<div class="button-group">
|
||
<button
|
||
class="ctrl-btn"
|
||
:class="{ active: admin.systemPhase === 'IDLE' }"
|
||
@click="setPhase('IDLE')"
|
||
>
|
||
待机画面
|
||
</button>
|
||
<button
|
||
class="ctrl-btn"
|
||
:class="{ active: admin.systemPhase === 'VOTING' }"
|
||
@click="setPhase('VOTING')"
|
||
>
|
||
投票画面
|
||
</button>
|
||
<button
|
||
class="ctrl-btn"
|
||
:class="{ active: admin.systemPhase === 'LOTTERY' }"
|
||
@click="setPhase('LOTTERY')"
|
||
>
|
||
抽奖画面
|
||
</button>
|
||
<button
|
||
class="ctrl-btn"
|
||
:class="{ active: admin.systemPhase === 'RESULTS' }"
|
||
@click="setPhase('RESULTS')"
|
||
>
|
||
结果展示
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Music Control -->
|
||
<div class="control-group">
|
||
<h4>音乐控制</h4>
|
||
<div class="button-group">
|
||
<button
|
||
class="ctrl-btn"
|
||
:class="{ active: admin.musicPlaying }"
|
||
@click="toggleMusic"
|
||
>
|
||
{{ admin.musicPlaying ? '停止音乐' : '播放BGM' }}
|
||
</button>
|
||
<button class="ctrl-btn" @click="playFanfare">
|
||
播放礼花音效
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Emergency Reset -->
|
||
<div class="control-group danger-zone">
|
||
<h4>紧急操作</h4>
|
||
<div class="button-group">
|
||
<button class="ctrl-btn danger-outline" @click="openResetModal('voting')">
|
||
重置投票
|
||
</button>
|
||
<button class="ctrl-btn danger-outline" @click="openResetModal('lottery')">
|
||
重置抽奖
|
||
</button>
|
||
<button class="ctrl-btn danger" @click="openResetModal('all')">
|
||
全部重置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<!-- Reset Confirmation Modal -->
|
||
<div v-if="showResetModal" class="modal-overlay" @click.self="cancelReset">
|
||
<div class="modal">
|
||
<h3>确认重置</h3>
|
||
<p>
|
||
您即将重置
|
||
<strong>{{ resetScope === 'all' ? '所有数据' : resetScope === 'voting' ? '投票数据' : '抽奖数据' }}</strong>
|
||
</p>
|
||
<p class="warning-text">此操作不可撤销!</p>
|
||
<div class="modal-input">
|
||
<label>请输入 <code>RESET</code> 确认:</label>
|
||
<input
|
||
v-model="confirmResetCode"
|
||
type="text"
|
||
placeholder="RESET"
|
||
@keyup.enter="confirmReset"
|
||
/>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="ctrl-btn outline" @click="cancelReset">取消</button>
|
||
<button
|
||
class="ctrl-btn danger"
|
||
:disabled="confirmResetCode !== 'RESET'"
|
||
@click="confirmReset"
|
||
>
|
||
确认重置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
@use '../assets/styles/variables.scss' as *;
|
||
|
||
// Industrial color palette
|
||
$admin-bg: #0d0d0d;
|
||
$admin-surface: #1a1a1a;
|
||
$admin-border: #333;
|
||
$admin-text: #e0e0e0;
|
||
$admin-text-muted: #888;
|
||
$admin-primary: #3b82f6;
|
||
$admin-success: #22c55e;
|
||
$admin-warning: #f59e0b;
|
||
$admin-danger: #ef4444;
|
||
|
||
.admin-control {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: $admin-bg;
|
||
color: $admin-text;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
// Header
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
padding: 16px 24px;
|
||
background: $admin-surface;
|
||
border-bottom: 1px solid $admin-border;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
|
||
.back-btn {
|
||
background: transparent;
|
||
border: 1px solid $admin-border;
|
||
color: $admin-text-muted;
|
||
padding: 8px 16px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
border-color: $admin-text;
|
||
color: $admin-text;
|
||
}
|
||
}
|
||
|
||
.title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
}
|
||
|
||
.status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: $admin-text-muted;
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
|
||
&.online {
|
||
background: $admin-success;
|
||
box-shadow: 0 0 8px rgba($admin-success, 0.5);
|
||
}
|
||
|
||
&.offline {
|
||
background: #666;
|
||
}
|
||
}
|
||
|
||
.icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
&.phase-badge {
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
font-size: 12px;
|
||
|
||
&.idle { background: #333; color: #888; }
|
||
&.voting { background: rgba($admin-primary, 0.2); color: $admin-primary; }
|
||
&.lottery { background: rgba($admin-warning, 0.2); color: $admin-warning; }
|
||
&.results { background: rgba($admin-success, 0.2); color: $admin-success; }
|
||
}
|
||
}
|
||
}
|
||
|
||
.content {
|
||
padding: 24px;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
// Error Toast
|
||
.error-toast {
|
||
position: fixed;
|
||
top: 80px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: $admin-danger;
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
z-index: 200;
|
||
animation: slideDown 0.3s ease;
|
||
|
||
.close {
|
||
font-size: 18px;
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from { transform: translateX(-50%) translateY(-20px); opacity: 0; }
|
||
to { transform: translateX(-50%) translateY(0); opacity: 1; }
|
||
}
|
||
|
||
// Control Sections
|
||
.control-section {
|
||
background: $admin-surface;
|
||
border: 1px solid $admin-border;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
|
||
&.lottery-section {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
&.global-section {
|
||
grid-column: span 2;
|
||
}
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border-bottom: 1px solid $admin-border;
|
||
|
||
h2 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin: 0;
|
||
}
|
||
|
||
.section-status {
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
background: #333;
|
||
color: #888;
|
||
|
||
&.active {
|
||
background: rgba($admin-success, 0.2);
|
||
color: $admin-success;
|
||
}
|
||
}
|
||
|
||
.round-indicator {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.round-badge {
|
||
padding: 4px 12px;
|
||
background: rgba($admin-warning, 0.2);
|
||
color: $admin-warning;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.phase-badge {
|
||
padding: 4px 12px;
|
||
background: #333;
|
||
color: #888;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.section-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
// Stats
|
||
.stat-row {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: $admin-text-muted;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: $admin-text;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
}
|
||
|
||
// Button Groups
|
||
.button-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.ctrl-btn {
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
border-radius: 8px;
|
||
border: 1px solid $admin-border;
|
||
background: #252525;
|
||
color: $admin-text;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: #333;
|
||
border-color: #444;
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
&.active {
|
||
background: rgba($admin-primary, 0.2);
|
||
border-color: $admin-primary;
|
||
color: $admin-primary;
|
||
}
|
||
|
||
&.primary {
|
||
background: $admin-primary;
|
||
border-color: $admin-primary;
|
||
color: white;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: lighten($admin-primary, 5%);
|
||
}
|
||
}
|
||
|
||
&.success {
|
||
background: $admin-success;
|
||
border-color: $admin-success;
|
||
color: white;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: lighten($admin-success, 5%);
|
||
}
|
||
}
|
||
|
||
&.warning {
|
||
background: $admin-warning;
|
||
border-color: $admin-warning;
|
||
color: #000;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: lighten($admin-warning, 5%);
|
||
}
|
||
}
|
||
|
||
&.danger {
|
||
background: $admin-danger;
|
||
border-color: $admin-danger;
|
||
color: white;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: lighten($admin-danger, 5%);
|
||
}
|
||
}
|
||
|
||
&.danger-outline {
|
||
background: transparent;
|
||
border-color: $admin-danger;
|
||
color: $admin-danger;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: rgba($admin-danger, 0.1);
|
||
}
|
||
}
|
||
|
||
&.outline {
|
||
background: transparent;
|
||
border-color: $admin-border;
|
||
color: $admin-text-muted;
|
||
|
||
&:hover:not(:disabled) {
|
||
border-color: $admin-text;
|
||
color: $admin-text;
|
||
}
|
||
}
|
||
|
||
&.large {
|
||
padding: 16px 40px;
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
|
||
// Prize Info
|
||
.prize-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
padding: 16px 20px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
|
||
.prize-level {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: $color-gold;
|
||
}
|
||
|
||
.prize-name {
|
||
font-size: 16px;
|
||
color: $admin-text;
|
||
}
|
||
|
||
.prize-count {
|
||
font-size: 14px;
|
||
color: $admin-text-muted;
|
||
}
|
||
|
||
.prize-filter {
|
||
padding: 4px 12px;
|
||
background: rgba($admin-warning, 0.2);
|
||
color: $admin-warning;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
// Lottery Controls
|
||
.lottery-controls {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 20px 0;
|
||
|
||
.countdown {
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
}
|
||
|
||
.complete-controls {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
|
||
// Winners Display
|
||
.winners-display {
|
||
margin-top: 20px;
|
||
padding: 16px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border-radius: 8px;
|
||
|
||
h4 {
|
||
font-size: 14px;
|
||
color: $admin-text-muted;
|
||
margin: 0 0 12px 0;
|
||
}
|
||
}
|
||
|
||
.winners-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.winner-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 8px 16px;
|
||
background: rgba($color-gold, 0.1);
|
||
border: 1px solid rgba($color-gold, 0.3);
|
||
border-radius: 8px;
|
||
|
||
.winner-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: $color-gold;
|
||
}
|
||
|
||
.winner-dept {
|
||
font-size: 12px;
|
||
color: $admin-text-muted;
|
||
}
|
||
}
|
||
|
||
// Round Progress
|
||
.round-progress {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
margin-top: 24px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid $admin-border;
|
||
}
|
||
|
||
.round-dot {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
background: #252525;
|
||
border: 2px solid $admin-border;
|
||
color: $admin-text-muted;
|
||
|
||
&.active {
|
||
background: rgba($admin-warning, 0.2);
|
||
border-color: $admin-warning;
|
||
color: $admin-warning;
|
||
}
|
||
|
||
&.completed {
|
||
background: rgba($admin-success, 0.2);
|
||
border-color: $admin-success;
|
||
color: $admin-success;
|
||
}
|
||
}
|
||
|
||
// Control Groups
|
||
.control-group {
|
||
margin-bottom: 24px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
h4 {
|
||
font-size: 12px;
|
||
color: $admin-text-muted;
|
||
text-transform: uppercase;
|
||
margin: 0 0 12px 0;
|
||
}
|
||
|
||
&.danger-zone {
|
||
padding-top: 20px;
|
||
border-top: 1px solid rgba($admin-danger, 0.3);
|
||
|
||
h4 {
|
||
color: $admin-danger;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Modal
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal {
|
||
background: $admin-surface;
|
||
border: 1px solid $admin-border;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
width: 400px;
|
||
max-width: 90vw;
|
||
|
||
h3 {
|
||
font-size: 18px;
|
||
margin: 0 0 16px 0;
|
||
}
|
||
|
||
p {
|
||
color: $admin-text-muted;
|
||
margin: 0 0 12px 0;
|
||
|
||
strong {
|
||
color: $admin-text;
|
||
}
|
||
}
|
||
|
||
.warning-text {
|
||
color: $admin-danger;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.modal-input {
|
||
margin: 20px 0;
|
||
|
||
label {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: $admin-text-muted;
|
||
margin-bottom: 8px;
|
||
|
||
code {
|
||
background: #333;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
color: $admin-danger;
|
||
}
|
||
}
|
||
|
||
input {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: #252525;
|
||
border: 1px solid $admin-border;
|
||
border-radius: 8px;
|
||
color: $admin-text;
|
||
font-size: 16px;
|
||
|
||
&:focus {
|
||
outline: none;
|
||
border-color: $admin-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: 20px;
|
||
}
|
||
</style>
|