- Add getStats() method to participantService for tag distribution - Update participants API to return tagDistribution statistics - Load existing participants on AdminControl mount - Add mobile responsive styles for import section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1842 lines
41 KiB
Vue
1842 lines
41 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue';
|
||
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';
|
||
|
||
const router = useRouter();
|
||
const admin = useAdminStore();
|
||
|
||
// Local UI state
|
||
const confirmResetCode = ref('');
|
||
const showResetModal = ref(false);
|
||
const resetScope = ref<'all' | 'voting' | 'lottery'>('all');
|
||
|
||
// Prize configuration state
|
||
const showPrizeConfig = ref(false);
|
||
const prizeConfigLoading = ref(false);
|
||
const prizeConfigSaving = ref(false);
|
||
const editingPrizes = ref<PrizeConfig[]>([]);
|
||
|
||
// Load prize configuration from server
|
||
async function loadPrizeConfig() {
|
||
prizeConfigLoading.value = true;
|
||
try {
|
||
const res = await fetch('/api/admin/prizes');
|
||
const data = await res.json();
|
||
if (data.success && data.data?.prizes) {
|
||
editingPrizes.value = data.data.prizes.map((p: any) => ({ ...p }));
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load prize config:', e);
|
||
} finally {
|
||
prizeConfigLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// Save prize configuration to server
|
||
async function savePrizeConfig() {
|
||
prizeConfigSaving.value = true;
|
||
try {
|
||
const res = await fetch('/api/admin/prizes', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prizes: editingPrizes.value }),
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
showPrizeConfig.value = false;
|
||
} else {
|
||
alert('保存失败: ' + (data.error || '未知错误'));
|
||
}
|
||
} catch (e) {
|
||
alert('保存失败: ' + (e as Error).message);
|
||
} finally {
|
||
prizeConfigSaving.value = false;
|
||
}
|
||
}
|
||
|
||
// Open prize configuration modal
|
||
function openPrizeConfig() {
|
||
showPrizeConfig.value = true;
|
||
loadPrizeConfig();
|
||
}
|
||
|
||
// Participant import state
|
||
const importFile = ref<File | null>(null);
|
||
const importing = ref(false);
|
||
const importResult = ref<{
|
||
success: boolean;
|
||
totalCount: number;
|
||
importedCount: number;
|
||
tagDistribution: Record<string, number>;
|
||
errors: string[];
|
||
} | null>(null);
|
||
|
||
// Handle file selection
|
||
function handleFileSelect(event: Event) {
|
||
const input = event.target as HTMLInputElement;
|
||
if (input.files && input.files.length > 0) {
|
||
importFile.value = input.files[0];
|
||
}
|
||
}
|
||
|
||
// Import participants from Excel
|
||
async function importParticipants() {
|
||
if (!importFile.value) return;
|
||
|
||
importing.value = true;
|
||
importResult.value = null;
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', importFile.value);
|
||
|
||
const response = await fetch('/api/admin/participants/import', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
const data = await response.json();
|
||
// 确保保留 success 字段,后端返回结构为 { success: true, data: {...} }
|
||
importResult.value = {
|
||
success: data.success ?? data.data?.success ?? false,
|
||
totalCount: data.data?.totalCount ?? data.totalCount ?? 0,
|
||
importedCount: data.data?.importedCount ?? data.importedCount ?? 0,
|
||
tagDistribution: data.data?.tagDistribution ?? data.tagDistribution ?? {},
|
||
errors: data.data?.errors ?? data.errors ?? [],
|
||
};
|
||
} catch (error) {
|
||
importResult.value = {
|
||
success: false,
|
||
totalCount: 0,
|
||
importedCount: 0,
|
||
tagDistribution: {},
|
||
errors: [(error as Error).message],
|
||
};
|
||
} finally {
|
||
importing.value = false;
|
||
}
|
||
}
|
||
|
||
// Tag display names
|
||
const tagLabels: Record<string, string> = {
|
||
'6070': '60/70后',
|
||
'80': '80后',
|
||
'90': '90后',
|
||
'horse': '属马',
|
||
};
|
||
|
||
// Load existing participants from server
|
||
async function loadParticipants() {
|
||
try {
|
||
const response = await fetch('/api/admin/participants');
|
||
const data = await response.json();
|
||
if (data.success && data.data?.count > 0) {
|
||
importResult.value = {
|
||
success: true,
|
||
totalCount: data.data.count,
|
||
importedCount: data.data.count,
|
||
tagDistribution: data.data.tagDistribution || {},
|
||
errors: [],
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load participants:', error);
|
||
}
|
||
}
|
||
|
||
// 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');
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
// QR Code display control
|
||
function showEntryQR() {
|
||
const socket = admin.getSocket();
|
||
if (socket) {
|
||
socket.emit('display:show_entry_qr' as any, {});
|
||
}
|
||
}
|
||
|
||
function hideQR() {
|
||
const socket = admin.getSocket();
|
||
if (socket) {
|
||
socket.emit('display:hide_qr' as any, );
|
||
}
|
||
}
|
||
|
||
// Computed helpers
|
||
const currentPrizeConfig = computed(() => {
|
||
// 优先使用从服务器加载的配置
|
||
if (editingPrizes.value.length > 0) {
|
||
return editingPrizes.value.find(p => p.round === admin.lotteryRound) || editingPrizes.value[0];
|
||
}
|
||
// 降级到硬编码配置
|
||
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 '未知';
|
||
}
|
||
});
|
||
|
||
// Award type labels
|
||
const awardLabels: Record<string, string> = {
|
||
creative: '最佳创意奖',
|
||
visual: '最佳视觉奖',
|
||
atmosphere: '最佳气氛奖',
|
||
performance: '最佳表演奖',
|
||
teamwork: '最佳团队奖',
|
||
popularity: '最受欢迎奖',
|
||
potential: '最具潜力奖',
|
||
};
|
||
|
||
// Compute award statistics grouped by award type
|
||
const awardStats = computed(() => {
|
||
const stats: Record<string, Array<{ programName: string; teamName: string; votes: number; programId: string }>> = {};
|
||
|
||
// Initialize all award types
|
||
Object.keys(awardLabels).forEach(type => {
|
||
stats[type] = [];
|
||
});
|
||
|
||
// Aggregate stamps by award type
|
||
admin.programs.forEach(program => {
|
||
if (program.stamps && program.stamps.length > 0) {
|
||
// Count stamps by type for this program
|
||
const typeCount: Record<string, number> = {};
|
||
program.stamps.forEach((stamp: any) => {
|
||
const type = stamp.ticketType || 'unknown';
|
||
typeCount[type] = (typeCount[type] || 0) + 1;
|
||
});
|
||
|
||
// Add to stats
|
||
Object.entries(typeCount).forEach(([type, count]) => {
|
||
if (stats[type]) {
|
||
stats[type].push({
|
||
programId: program.id,
|
||
programName: program.name,
|
||
teamName: program.teamName,
|
||
votes: count,
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Sort each award type by votes descending
|
||
Object.keys(stats).forEach(type => {
|
||
stats[type].sort((a, b) => b.votes - a.votes);
|
||
});
|
||
|
||
return stats;
|
||
});
|
||
|
||
// Lifecycle
|
||
onMounted(() => {
|
||
admin.connect();
|
||
// 加载服务器奖项配置
|
||
loadPrizeConfig();
|
||
// 加载已导入的参与者数据
|
||
loadParticipants();
|
||
});
|
||
</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 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>
|
||
<button class="prize-config-btn" @click="openPrizeConfig" title="配置奖项">
|
||
⚙️ 配置
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Prize Configuration Modal -->
|
||
<div v-if="showPrizeConfig" class="modal-overlay" @click.self="showPrizeConfig = false">
|
||
<div class="modal-content prize-config-modal">
|
||
<div class="modal-header">
|
||
<h3>奖项配置</h3>
|
||
<button class="close-btn" @click="showPrizeConfig = false">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div v-if="prizeConfigLoading" class="loading">加载中...</div>
|
||
<div v-else class="prize-list">
|
||
<div v-for="prize in editingPrizes" :key="prize.round" class="prize-edit-item">
|
||
<div class="prize-round">第 {{ prize.round }} 轮</div>
|
||
<input v-model="prize.level" placeholder="等级名称" class="prize-input" />
|
||
<input v-model="prize.name" placeholder="奖品名称" class="prize-input prize-input-wide" />
|
||
<input v-model.number="prize.winnerCount" type="number" min="1" class="prize-input prize-input-small" />
|
||
<span class="prize-unit">名</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="ctrl-btn outline" @click="showPrizeConfig = false">取消</button>
|
||
<button class="ctrl-btn primary" :disabled="prizeConfigSaving" @click="savePrizeConfig">
|
||
{{ prizeConfigSaving ? '保存中...' : '保存' }}
|
||
</button>
|
||
</div>
|
||
</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">
|
||
{{ admin.stopButtonCountdown }}s
|
||
</span>
|
||
<span v-else>停止抽奖</span>
|
||
</button>
|
||
|
||
<!-- REVEAL State -->
|
||
<div v-if="admin.lotterySubPhase === 'REVEAL'" 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>
|
||
|
||
<!-- 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: Participant Import -->
|
||
<section class="control-section import-section">
|
||
<div class="section-header">
|
||
<h2>🎯 抽奖名单导入</h2>
|
||
<span v-if="importResult" class="section-status" :class="{ active: importResult.success }">
|
||
{{ importResult.success ? `已导入 ${importResult.importedCount} 人` : '导入失败' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="section-body">
|
||
<div class="import-controls">
|
||
<label class="file-input-wrapper">
|
||
<input
|
||
type="file"
|
||
accept=".xlsx,.xls"
|
||
@change="handleFileSelect"
|
||
class="file-input"
|
||
/>
|
||
<span class="file-input-label">
|
||
{{ importFile ? importFile.name : '选择 Excel 文件' }}
|
||
</span>
|
||
</label>
|
||
<button
|
||
class="ctrl-btn primary"
|
||
:disabled="!importFile || importing"
|
||
@click="importParticipants"
|
||
>
|
||
{{ importing ? '导入中...' : '开始导入' }}
|
||
</button>
|
||
</div>
|
||
|
||
<p class="import-hint">支持格式:岗位 | 姓名 | 年份</p>
|
||
|
||
<!-- Import Result -->
|
||
<div v-if="importResult" class="import-result" :class="{ error: !importResult.success }">
|
||
<div v-if="importResult.success" class="result-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-label">总行数</span>
|
||
<span class="stat-value">{{ importResult.totalCount }}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">成功导入</span>
|
||
<span class="stat-value">{{ importResult.importedCount }}</span>
|
||
</div>
|
||
<div v-for="(count, tag) in importResult.tagDistribution" :key="tag" class="stat-item">
|
||
<span class="stat-label">{{ tagLabels[tag] || tag }}</span>
|
||
<span class="stat-value">{{ count }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="importResult.errors.length > 0" class="result-errors">
|
||
<p v-for="(err, i) in importResult.errors.slice(0, 5)" :key="i" class="error-line">{{ err }}</p>
|
||
<p v-if="importResult.errors.length > 5" class="error-more">...还有 {{ importResult.errors.length - 5 }} 条错误</p>
|
||
</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>
|
||
|
||
<!-- QR Code Display Control -->
|
||
<div class="control-group">
|
||
<h4>大屏二维码</h4>
|
||
<div class="button-group">
|
||
<button class="ctrl-btn primary" @click="showEntryQR">
|
||
显示入场二维码
|
||
</button>
|
||
<button class="ctrl-btn outline" @click="hideQR">
|
||
隐藏二维码
|
||
</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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Award Statistics Styles
|
||
.award-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.award-group {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid $admin-border;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.award-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 14px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border-bottom: 1px solid $admin-border;
|
||
|
||
.award-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: $color-gold;
|
||
}
|
||
|
||
.award-total {
|
||
font-size: 12px;
|
||
color: $admin-text-muted;
|
||
background: rgba($admin-primary, 0.15);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.award-programs {
|
||
padding: 8px;
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.program-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto auto;
|
||
gap: 8px;
|
||
align-items: center;
|
||
padding: 6px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
.program-name {
|
||
color: $admin-text;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.program-team {
|
||
color: $admin-text-muted;
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.program-votes {
|
||
color: $admin-success;
|
||
font-weight: 600;
|
||
min-width: 24px;
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.no-votes {
|
||
padding: 12px;
|
||
text-align: center;
|
||
color: $admin-text-muted;
|
||
font-size: 12px;
|
||
font-style: italic;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
// ==========================================
|
||
// Mobile Responsive Styles
|
||
// ==========================================
|
||
@media (max-width: 768px) {
|
||
// Header
|
||
.header {
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
|
||
.title {
|
||
font-size: 18px;
|
||
order: -1;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.back-btn {
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.status-bar {
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
width: 100%;
|
||
}
|
||
|
||
.status-item {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
// Content Grid
|
||
.content {
|
||
grid-template-columns: 1fr;
|
||
padding: 16px;
|
||
gap: 16px;
|
||
}
|
||
|
||
// Control Sections
|
||
.control-section {
|
||
&.lottery-section,
|
||
&.global-section {
|
||
grid-column: span 1;
|
||
}
|
||
}
|
||
|
||
.section-header {
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 12px 16px;
|
||
|
||
h2 {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.round-indicator {
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
.section-body {
|
||
padding: 16px;
|
||
}
|
||
|
||
// Stats
|
||
.stat-row {
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.stat-item {
|
||
.stat-value {
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
|
||
// Buttons
|
||
.button-group {
|
||
gap: 8px;
|
||
}
|
||
|
||
.ctrl-btn {
|
||
padding: 10px 16px;
|
||
font-size: 13px;
|
||
flex: 1;
|
||
min-width: 100px;
|
||
text-align: center;
|
||
|
||
&.large {
|
||
padding: 14px 24px;
|
||
font-size: 16px;
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
// Prize Info
|
||
.prize-info {
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
text-align: center;
|
||
|
||
.prize-level {
|
||
font-size: 18px;
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
// Lottery Controls
|
||
.lottery-controls {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.complete-controls {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
width: 100%;
|
||
|
||
.ctrl-btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
// Winners
|
||
.winners-list {
|
||
gap: 8px;
|
||
}
|
||
|
||
.winner-item {
|
||
padding: 6px 12px;
|
||
|
||
.winner-name {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
// Round Progress
|
||
.round-progress {
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
}
|
||
|
||
.round-dot {
|
||
width: 28px;
|
||
height: 28px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
// Program List
|
||
.program-list {
|
||
max-height: 300px;
|
||
}
|
||
|
||
.program-item {
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
// Control Groups
|
||
.control-group {
|
||
margin-bottom: 20px;
|
||
|
||
h4 {
|
||
font-size: 11px;
|
||
margin-bottom: 10px;
|
||
}
|
||
}
|
||
|
||
// Modal
|
||
.modal {
|
||
width: 95vw;
|
||
padding: 20px;
|
||
|
||
h3 {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.modal-input input {
|
||
padding: 10px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.modal-actions {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
|
||
.ctrl-btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Import Section Styles
|
||
.import-section {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.import-controls {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.file-input-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
|
||
.file-input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-input-label {
|
||
display: inline-block;
|
||
padding: 10px 20px;
|
||
background: #252525;
|
||
border: 1px dashed #444;
|
||
border-radius: 8px;
|
||
color: #888;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
border-color: #666;
|
||
color: #ccc;
|
||
}
|
||
}
|
||
}
|
||
|
||
.import-hint {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.import-result {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
|
||
&.error {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border-color: rgba(239, 68, 68, 0.3);
|
||
}
|
||
}
|
||
|
||
.result-stats {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 24px;
|
||
|
||
.stat-item {
|
||
.stat-value {
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.result-errors {
|
||
margin-top: 12px;
|
||
|
||
.error-line {
|
||
font-size: 12px;
|
||
color: #ef4444;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.error-more {
|
||
font-size: 12px;
|
||
color: #888;
|
||
font-style: italic;
|
||
}
|
||
}
|
||
|
||
// Import Section Mobile Styles
|
||
@media (max-width: 768px) {
|
||
.import-section {
|
||
grid-column: span 1;
|
||
}
|
||
|
||
.import-controls {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 12px;
|
||
}
|
||
|
||
.file-input-wrapper {
|
||
width: 100%;
|
||
|
||
.file-input-label {
|
||
width: 100%;
|
||
text-align: center;
|
||
box-sizing: border-box;
|
||
}
|
||
}
|
||
|
||
.result-stats {
|
||
gap: 12px;
|
||
|
||
.stat-item {
|
||
min-width: calc(33% - 8px);
|
||
|
||
.stat-value {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.import-result {
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
// Prize Config Button & Modal
|
||
.prize-config-btn {
|
||
margin-top: 12px;
|
||
padding: 6px 12px;
|
||
background: #333;
|
||
border: 1px solid #555;
|
||
border-radius: 6px;
|
||
color: #ccc;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
background: #444;
|
||
border-color: #666;
|
||
}
|
||
}
|
||
|
||
.prize-config-modal {
|
||
width: 600px;
|
||
max-width: 90vw;
|
||
}
|
||
|
||
.prize-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.prize-edit-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background: #252525;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.prize-round {
|
||
min-width: 60px;
|
||
font-weight: bold;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.prize-input {
|
||
padding: 8px 12px;
|
||
background: #1a1a1a;
|
||
border: 1px solid #444;
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #f59e0b;
|
||
}
|
||
}
|
||
|
||
.prize-input-wide {
|
||
flex: 1;
|
||
}
|
||
|
||
.prize-input-small {
|
||
width: 60px;
|
||
text-align: center;
|
||
}
|
||
|
||
.prize-unit {
|
||
color: #888;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
color: #888;
|
||
padding: 20px;
|
||
}
|
||
</style>
|