You've already forked rollingDraw
初始化提交
This commit is contained in:
20
src/App.vue
Normal file
20
src/App.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 主应用组件,只负责路由渲染
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
24
src/main.js
Normal file
24
src/main.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './styles/variables.css'
|
||||
import './styles/global.css'
|
||||
import './styles/element-plus.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.mount('#app')
|
||||
44
src/router/index.js
Normal file
44
src/router/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/admin/participants'
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: () => import('@/views/AdminLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'participants',
|
||||
name: 'Participants',
|
||||
component: () => import('@/views/admin/Participants.vue')
|
||||
},
|
||||
{
|
||||
path: 'prizes',
|
||||
name: 'Prizes',
|
||||
component: () => import('@/views/admin/Prizes.vue')
|
||||
},
|
||||
{
|
||||
path: 'rounds',
|
||||
name: 'Rounds',
|
||||
component: () => import('@/views/admin/Rounds.vue')
|
||||
},
|
||||
{
|
||||
path: 'winners',
|
||||
name: 'Winners',
|
||||
component: () => import('@/views/admin/Winners.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/display',
|
||||
name: 'Display',
|
||||
component: () => import('@/views/Display.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
490
src/store/index.js
Normal file
490
src/store/index.js
Normal file
@@ -0,0 +1,490 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { indexedDB } from '@utils/indexedDB'
|
||||
|
||||
export const useLotteryStore = defineStore('lottery', () => {
|
||||
// 初始化数据
|
||||
const fields = ref([])
|
||||
const participants = ref([])
|
||||
const prizes = ref([])
|
||||
const rounds = ref([])
|
||||
const winners = ref([])
|
||||
const isRolling = ref(false)
|
||||
const currentRound = ref(null)
|
||||
const displayMode = ref('scroll')
|
||||
const backgroundImage = ref('')
|
||||
const columnsPerRow = ref(3) // 每行显示的人数
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// 从IndexedDB初始化数据
|
||||
const initialize = async () => {
|
||||
if (isInitialized.value) return
|
||||
|
||||
try {
|
||||
console.log('Initializing from IndexedDB...')
|
||||
const data = await indexedDB.getAll()
|
||||
console.log('Retrieved data keys:', Object.keys(data))
|
||||
|
||||
fields.value = data.lottery_fields || []
|
||||
participants.value = data.lottery_participants || []
|
||||
prizes.value = data.lottery_prizes || []
|
||||
rounds.value = data.lottery_rounds || []
|
||||
winners.value = data.lottery_winners || []
|
||||
isRolling.value = data.lottery_isRolling === 'true'
|
||||
currentRound.value = data.lottery_currentRound || null
|
||||
displayMode.value = data.lottery_displayMode || 'scroll'
|
||||
backgroundImage.value = data.lottery_backgroundImage || ''
|
||||
columnsPerRow.value = data.lottery_columnsPerRow || 3
|
||||
|
||||
console.log('Initialized background image:', backgroundImage.value ? 'Yes' : 'No')
|
||||
console.log('Initialization completed')
|
||||
|
||||
// 如果当前不是滚动状态,重置显示模式
|
||||
if (!isRolling.value && displayMode.value === 'result') {
|
||||
displayMode.value = 'scroll'
|
||||
await saveData('lottery_displayMode', displayMode.value)
|
||||
}
|
||||
|
||||
isInitialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize from IndexedDB:', error)
|
||||
// 使用默认值
|
||||
fields.value = [{key: 'name', label: '姓名', required: true}]
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 保存单个数据到IndexedDB
|
||||
const saveData = async (key, value) => {
|
||||
try {
|
||||
console.log(`Saving ${key} to IndexedDB...`)
|
||||
// 深拷贝数据以避免Vue响应式包装器
|
||||
const clonedValue = JSON.parse(JSON.stringify(value))
|
||||
await indexedDB.set(key, clonedValue)
|
||||
|
||||
// 触发localStorage事件通知其他标签页
|
||||
localStorage.setItem('lottery_data_changed', Date.now().toString())
|
||||
|
||||
console.log(`Successfully saved ${key}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to save ${key} to IndexedDB:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 监听localStorage变化
|
||||
const handleStorageChange = (e) => {
|
||||
if (e.key === 'lottery_data_changed') {
|
||||
console.log('Storage change detected, reloading data...')
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
}
|
||||
|
||||
// ============ 字段管理 ============
|
||||
const addField = async (field) => {
|
||||
const newField = {
|
||||
id: Date.now(),
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
required: field.required || false
|
||||
}
|
||||
fields.value.push(newField)
|
||||
await saveData('lottery_fields', fields.value)
|
||||
}
|
||||
|
||||
const updateField = async (field) => {
|
||||
const index = fields.value.findIndex(f => f.id === field.id)
|
||||
if (index > -1) {
|
||||
fields.value[index] = field
|
||||
await saveData('lottery_fields', fields.value)
|
||||
}
|
||||
}
|
||||
|
||||
const removeField = async (id) => {
|
||||
const index = fields.value.findIndex(f => f.id === id)
|
||||
if (index > -1) {
|
||||
fields.value.splice(index, 1)
|
||||
await saveData('lottery_fields', fields.value)
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 参与者管理 ============
|
||||
const addParticipant = async (participant) => {
|
||||
participants.value.push(participant)
|
||||
await saveData('lottery_participants', participants.value)
|
||||
}
|
||||
|
||||
const updateParticipant = async (participant) => {
|
||||
const index = participants.value.findIndex(p => p.id === participant.id)
|
||||
if (index > -1) {
|
||||
participants.value[index] = participant
|
||||
await saveData('lottery_participants', participants.value)
|
||||
}
|
||||
}
|
||||
|
||||
const removeParticipant = async (id) => {
|
||||
const index = participants.value.findIndex(p => p.id === id)
|
||||
if (index > -1) {
|
||||
participants.value.splice(index, 1)
|
||||
await saveData('lottery_participants', participants.value)
|
||||
}
|
||||
}
|
||||
|
||||
const clearParticipants = async () => {
|
||||
participants.value = []
|
||||
await saveData('lottery_participants', participants.value)
|
||||
}
|
||||
|
||||
const importParticipantsFromFile = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const content = e.target.result
|
||||
const lines = content.split('\n').map(l => l.trim()).filter(l => l)
|
||||
|
||||
// 检测是否是CSV格式(包含逗号)
|
||||
const isCSV = lines.some(line => line.includes(','))
|
||||
|
||||
if (isCSV) {
|
||||
// CSV格式导入
|
||||
const headers = lines[0].split(',').map(h => h.trim())
|
||||
const data = []
|
||||
|
||||
// 自动添加缺失的字段到配置
|
||||
headers.forEach(header => {
|
||||
if (!fields.value.some(f => f.key === header)) {
|
||||
fields.value.push({
|
||||
id: Date.now() + Math.random(),
|
||||
key: header,
|
||||
label: header,
|
||||
required: false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 保存更新后的字段配置
|
||||
await saveData('lottery_fields', fields.value)
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(',').map(v => v.trim())
|
||||
const participant = { id: Date.now() + i }
|
||||
headers.forEach((header, index) => {
|
||||
participant[header] = values[index] || ''
|
||||
})
|
||||
data.push(participant)
|
||||
}
|
||||
|
||||
participants.value.push(...data)
|
||||
await saveData('lottery_participants', participants.value)
|
||||
resolve(data.length)
|
||||
} else {
|
||||
// 简单文本格式导入(每行一个姓名)
|
||||
const data = lines.map(line => ({
|
||||
id: Date.now() + Math.random(),
|
||||
name: line
|
||||
}))
|
||||
participants.value.push(...data)
|
||||
await saveData('lottery_participants', participants.value)
|
||||
resolve(data.length)
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
reader.onerror = () => reject(new Error('文件读取失败'))
|
||||
reader.readAsText(file)
|
||||
})
|
||||
}
|
||||
|
||||
const exportParticipants = () => {
|
||||
const headers = fields.value.map(f => f.label)
|
||||
const rows = participants.value.map(p => headers.map(h => p[h] || '').join(','))
|
||||
|
||||
const csv = [headers.join(','), ...rows].join('\n')
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `抽奖名单_${new Date().toLocaleDateString()}.csv`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// ============ 奖品管理 ============
|
||||
const addPrize = async (prize) => {
|
||||
prizes.value.push({
|
||||
id: Date.now(),
|
||||
name: prize.name,
|
||||
stock: parseInt(prize.stock) || 0,
|
||||
used: 0
|
||||
})
|
||||
await saveData('lottery_prizes', prizes.value)
|
||||
}
|
||||
|
||||
const updatePrize = async (prize) => {
|
||||
const index = prizes.value.findIndex(p => p.id === prize.id)
|
||||
if (index > -1) {
|
||||
prizes.value[index] = prize
|
||||
await saveData('lottery_prizes', prizes.value)
|
||||
}
|
||||
}
|
||||
|
||||
const removePrize = async (id) => {
|
||||
const index = prizes.value.findIndex(p => p.id === id)
|
||||
if (index > -1) {
|
||||
prizes.value.splice(index, 1)
|
||||
await saveData('lottery_prizes', prizes.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getPrizeAvailable = (prizeId) => {
|
||||
const prize = prizes.value.find(p => p.id === prizeId)
|
||||
return prize ? prize.stock - prize.used : 0
|
||||
}
|
||||
|
||||
// ============ 轮次管理 ============
|
||||
const addRound = async (round) => {
|
||||
rounds.value.push({
|
||||
id: Date.now(),
|
||||
name: round.name,
|
||||
prizeId: round.prizeId,
|
||||
count: parseInt(round.count) || 1,
|
||||
completed: false
|
||||
})
|
||||
await saveData('lottery_rounds', rounds.value)
|
||||
}
|
||||
|
||||
const updateRound = async (round) => {
|
||||
const index = rounds.value.findIndex(r => r.id === round.id)
|
||||
if (index > -1) {
|
||||
rounds.value[index] = round
|
||||
await saveData('lottery_rounds', rounds.value)
|
||||
}
|
||||
}
|
||||
|
||||
const removeRound = async (id) => {
|
||||
const index = rounds.value.findIndex(r => r.id === id)
|
||||
if (index > -1) {
|
||||
rounds.value.splice(index, 1)
|
||||
await saveData('lottery_rounds', rounds.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getPrizeForRound = (roundId) => {
|
||||
const round = rounds.value.find(r => r.id === roundId)
|
||||
if (!round) return null
|
||||
return prizes.value.find(p => p.id === round.prizeId)
|
||||
}
|
||||
|
||||
// ============ 抽奖控制 ============
|
||||
const startLottery = async (round) => {
|
||||
const prize = getPrizeForRound(round.id)
|
||||
if (!prize) {
|
||||
throw new Error('奖品不存在')
|
||||
}
|
||||
if (prize.used >= prize.stock) {
|
||||
throw new Error('奖品库存不足')
|
||||
}
|
||||
if (round.completed) {
|
||||
throw new Error('该轮次已完成')
|
||||
}
|
||||
if (participants.value.length === 0) {
|
||||
throw new Error('暂无名单')
|
||||
}
|
||||
|
||||
// 重置显示模式为滚动模式
|
||||
displayMode.value = 'scroll'
|
||||
|
||||
// 设置滚动状态
|
||||
isRolling.value = true
|
||||
currentRound.value = round
|
||||
|
||||
// 保存状态
|
||||
await saveData('lottery_displayMode', displayMode.value)
|
||||
await saveData('lottery_isRolling', isRolling.value)
|
||||
await saveData('lottery_currentRound', currentRound.value)
|
||||
}
|
||||
|
||||
const stopLottery = async () => {
|
||||
if (!currentRound.value) return
|
||||
|
||||
const prize = getPrizeForRound(currentRound.value.id)
|
||||
if (!prize) {
|
||||
throw new Error('奖品不存在')
|
||||
}
|
||||
|
||||
// 随机选择中奖者
|
||||
const available = participants.value.filter(p => {
|
||||
return !winners.value.some(w => w.participantId === p.id)
|
||||
})
|
||||
|
||||
if (available.length < currentRound.value.count) {
|
||||
throw new Error(`可用人数不足,需要${currentRound.value.count}人,只有${available.length}人`)
|
||||
}
|
||||
|
||||
// Fisher-Yates洗牌算法
|
||||
for (let i = available.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[available[i], available[j]] = [available[j], available[i]]
|
||||
}
|
||||
|
||||
const selected = available.slice(0, currentRound.value.count)
|
||||
|
||||
// 保存中奖记录
|
||||
selected.forEach(p => {
|
||||
winners.value.push({
|
||||
id: Date.now() + Math.random(),
|
||||
participantId: p.id,
|
||||
participant: p, // 保存完整的参与者信息
|
||||
prizeName: prize.name,
|
||||
roundId: currentRound.value.id,
|
||||
roundName: currentRound.value.name,
|
||||
time: new Date().toLocaleString()
|
||||
})
|
||||
})
|
||||
|
||||
// 更新奖品使用数量
|
||||
prize.used += currentRound.value.count
|
||||
|
||||
// 更新轮次状态
|
||||
const roundIndex = rounds.value.findIndex(r => r.id === currentRound.value.id)
|
||||
if (roundIndex > -1) {
|
||||
rounds.value[roundIndex].completed = true
|
||||
}
|
||||
|
||||
// 停止滚动
|
||||
isRolling.value = false
|
||||
displayMode.value = 'result'
|
||||
currentRound.value = null
|
||||
|
||||
// 保存所有相关数据
|
||||
await saveData('lottery_winners', winners.value)
|
||||
await saveData('lottery_prizes', prizes.value)
|
||||
await saveData('lottery_rounds', rounds.value)
|
||||
await saveData('lottery_isRolling', isRolling.value)
|
||||
await saveData('lottery_currentRound', currentRound.value)
|
||||
await saveData('lottery_displayMode', displayMode.value)
|
||||
}
|
||||
|
||||
const resetLottery = async () => {
|
||||
winners.value = []
|
||||
prizes.value.forEach(p => p.used = 0)
|
||||
rounds.value.forEach(r => r.completed = false)
|
||||
isRolling.value = false
|
||||
currentRound.value = null
|
||||
displayMode.value = 'scroll'
|
||||
await saveData('lottery_winners', winners.value)
|
||||
await saveData('lottery_prizes', prizes.value)
|
||||
await saveData('lottery_rounds', rounds.value)
|
||||
await saveData('lottery_isRolling', isRolling.value)
|
||||
await saveData('lottery_currentRound', currentRound.value)
|
||||
await saveData('lottery_displayMode', displayMode.value)
|
||||
}
|
||||
|
||||
const exportWinners = () => {
|
||||
const headers = ['姓名', '奖品', '轮次', '时间']
|
||||
const rows = winners.value.map(w => [w.name, w.prizeName, w.roundName, w.time])
|
||||
|
||||
const csv = [headers.join(','), ...rows].join('\n')
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `中奖名单_${new Date().toLocaleDateString()}.csv`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// ============ 显示控制 ============
|
||||
const switchDisplayMode = async (mode) => {
|
||||
displayMode.value = mode
|
||||
await saveData('lottery_displayMode', displayMode.value)
|
||||
}
|
||||
|
||||
const setColumnsPerRow = async (columns) => {
|
||||
columnsPerRow.value = columns
|
||||
await saveData('lottery_columnsPerRow', columnsPerRow.value)
|
||||
}
|
||||
|
||||
const setBackgroundImage = async (imageData) => {
|
||||
try {
|
||||
console.log('setBackgroundImage called with image data length:', imageData ? imageData.length : 0)
|
||||
await saveData('lottery_backgroundImage', imageData)
|
||||
backgroundImage.value = imageData
|
||||
console.log('setBackgroundImage completed successfully')
|
||||
} catch (error) {
|
||||
console.error('Failed to save background image:', error)
|
||||
throw new Error('保存背景图片失败')
|
||||
}
|
||||
}
|
||||
|
||||
const clearBackgroundImage = async () => {
|
||||
try {
|
||||
await indexedDB.delete('lottery_backgroundImage')
|
||||
backgroundImage.value = ''
|
||||
} catch (error) {
|
||||
console.error('Failed to clear background image:', error)
|
||||
throw new Error('清除背景图片失败')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
fields,
|
||||
participants,
|
||||
prizes,
|
||||
rounds,
|
||||
winners,
|
||||
isRolling,
|
||||
currentRound,
|
||||
displayMode,
|
||||
backgroundImage,
|
||||
columnsPerRow,
|
||||
isInitialized,
|
||||
|
||||
// 初始化
|
||||
initialize,
|
||||
|
||||
// 字段管理
|
||||
addField,
|
||||
updateField,
|
||||
removeField,
|
||||
|
||||
// 参与者管理
|
||||
addParticipant,
|
||||
updateParticipant,
|
||||
removeParticipant,
|
||||
clearParticipants,
|
||||
importParticipantsFromFile,
|
||||
exportParticipants,
|
||||
|
||||
// 奖品管理
|
||||
addPrize,
|
||||
updatePrize,
|
||||
removePrize,
|
||||
getPrizeAvailable,
|
||||
|
||||
// 轮次管理
|
||||
addRound,
|
||||
updateRound,
|
||||
removeRound,
|
||||
getPrizeForRound,
|
||||
|
||||
// 抽奖控制
|
||||
startLottery,
|
||||
stopLottery,
|
||||
resetLottery,
|
||||
exportWinners,
|
||||
|
||||
// 显示控制
|
||||
switchDisplayMode,
|
||||
setColumnsPerRow,
|
||||
setBackgroundImage,
|
||||
clearBackgroundImage
|
||||
}
|
||||
})
|
||||
340
src/styles/element-plus.css
Normal file
340
src/styles/element-plus.css
Normal file
@@ -0,0 +1,340 @@
|
||||
/* Element Plus Custom Styles */
|
||||
|
||||
/* Element Plus Variables Override */
|
||||
:root {
|
||||
--el-color-primary: var(--color-primary);
|
||||
--el-color-primary-light-3: #8fa3f7;
|
||||
--el-color-primary-light-5: #b8c5f8;
|
||||
--el-color-primary-light-7: #dce0fa;
|
||||
--el-color-primary-light-8: #ebeefc;
|
||||
--el-color-primary-light-9: #f5f6fe;
|
||||
--el-color-primary-dark-2: #5369d6;
|
||||
|
||||
--el-color-success: var(--color-success);
|
||||
--el-color-warning: var(--color-warning);
|
||||
--el-color-danger: var(--color-danger);
|
||||
--el-color-info: var(--color-info);
|
||||
|
||||
--el-border-radius-base: var(--border-radius-md);
|
||||
--el-border-radius-small: var(--border-radius-sm);
|
||||
--el-border-radius-round: 20px;
|
||||
--el-border-radius-circle: 100%;
|
||||
|
||||
--el-font-size-base: var(--font-size-base);
|
||||
--el-font-size-small: var(--font-size-sm);
|
||||
--el-font-size-large: var(--font-size-lg);
|
||||
--el-font-size-extra-large: var(--font-size-xl);
|
||||
|
||||
--el-transition-duration: var(--transition-normal);
|
||||
}
|
||||
|
||||
/* Element Plus Button Customization */
|
||||
.el-button {
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.el-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-small);
|
||||
}
|
||||
|
||||
.el-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-button--primary:hover {
|
||||
background: linear-gradient(135deg, var(--color-primary-dark-2) 0%, var(--color-secondary) 100%);
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: var(--color-success);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: var(--color-danger);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-button--warning {
|
||||
background: var(--color-warning);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-button--info {
|
||||
background: var(--color-info);
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Element Plus Card Customization */
|
||||
.el-card {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-small);
|
||||
transition: all var(--transition-normal);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.el-card:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--spacing-lg) var(--spacing-xl);
|
||||
font-family: var(--font-family-secondary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
/* Element Plus Dialog Customization */
|
||||
.el-dialog {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-large);
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
||||
color: var(--color-text-white);
|
||||
padding: var(--spacing-xl) var(--spacing-2xl);
|
||||
border-radius: var(--border-radius-lg) var(--border-radius-lg) 0 0;
|
||||
}
|
||||
|
||||
.el-dialog__title {
|
||||
font-family: var(--font-family-secondary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: var(--spacing-lg) var(--spacing-2xl);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Element Plus Input Customization */
|
||||
.el-input__wrapper {
|
||||
border-radius: var(--border-radius-md);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.el-input__wrapper:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.el-input__wrapper.is-focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
/* Element Plus Select Customization */
|
||||
.el-select .el-input__wrapper {
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
.el-select-dropdown__item {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.is-selected {
|
||||
background: rgba(var(--color-primary-rgb), 0.1);
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
/* Element Plus Form Customization */
|
||||
.el-form-item__label {
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.el-form-item__error {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Element Plus Table Customization */
|
||||
.el-table {
|
||||
font-family: var(--font-family-primary);
|
||||
border-radius: var(--border-radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-table th {
|
||||
background: var(--color-background);
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-family: var(--font-family-secondary);
|
||||
font-size: var(--font-size-base);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.el-table td {
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-base);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.el-table .el-table__row:hover > td {
|
||||
background: rgba(var(--color-primary-rgb), 0.05);
|
||||
}
|
||||
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background: rgba(var(--color-primary-rgb), 0.03);
|
||||
}
|
||||
|
||||
.el-table__body tr.current-row > td {
|
||||
background: rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
/* Element Plus Upload Customization */
|
||||
.el-upload-dragger {
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 2px dashed var(--color-border);
|
||||
transition: all var(--transition-normal);
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.el-upload-dragger:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb), 0.05);
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Element Plus Message Customization */
|
||||
.el-message {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-large);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding: var(--spacing-lg) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.el-message__icon {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
/* Element Plus Message Box Customization */
|
||||
.el-message-box {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-large);
|
||||
}
|
||||
|
||||
.el-message-box__header {
|
||||
padding: var(--spacing-xl) var(--spacing-2xl);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.el-message-box__title {
|
||||
font-family: var(--font-family-secondary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.el-message-box__content {
|
||||
padding: var(--spacing-2xl);
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.el-message-box__btns {
|
||||
padding: var(--spacing-lg) var(--spacing-2xl);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Element Plus Checkbox Customization */
|
||||
.el-checkbox__label {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.el-checkbox__input.is-checked .el-checkbox__inner {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Element Plus Radio Customization */
|
||||
.el-radio__label {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.el-radio__input.is-checked .el-radio__inner {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Element Plus Switch Customization */
|
||||
.el-switch.is-checked .el-switch__core {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Element Plus Tag Customization */
|
||||
.el-tag {
|
||||
border-radius: var(--border-radius-md);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.el-tag--primary {
|
||||
background: rgba(var(--color-primary-rgb), 0.1);
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.el-tag--success {
|
||||
background: rgba(var(--color-success), 0.1);
|
||||
color: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.el-tag--warning {
|
||||
background: rgba(var(--color-warning), 0.1);
|
||||
color: var(--color-warning);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.el-tag--danger {
|
||||
background: rgba(var(--color-danger), 0.1);
|
||||
color: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.el-tag--info {
|
||||
background: rgba(var(--color-info), 0.1);
|
||||
color: var(--color-info);
|
||||
border-color: var(--color-info);
|
||||
}
|
||||
241
src/styles/global.css
Normal file
241
src/styles/global.css
Normal file
@@ -0,0 +1,241 @@
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-background);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-border-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-text-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: background var(--transition-normal);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-family-secondary);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-tight);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h1 { font-size: var(--font-size-4xl); }
|
||||
h2 { font-size: var(--font-size-3xl); }
|
||||
h3 { font-size: var(--font-size-2xl); }
|
||||
h4 { font-size: var(--font-size-xl); }
|
||||
h5 { font-size: var(--font-size-lg); }
|
||||
h6 { font-size: var(--font-size-base); }
|
||||
|
||||
p {
|
||||
margin-bottom: var(--spacing-md);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-normal);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul, ol {
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding-left: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: var(--spacing-sm);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
/* Code */
|
||||
code {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--color-border);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--color-border);
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--border-radius-md);
|
||||
overflow-x: auto;
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.text-primary { color: var(--color-primary); }
|
||||
.text-secondary { color: var(--color-secondary); }
|
||||
.text-success { color: var(--color-success); }
|
||||
.text-danger { color: var(--color-danger); }
|
||||
.text-warning { color: var(--color-warning); }
|
||||
.text-light { color: var(--color-text-light); }
|
||||
|
||||
.bg-primary { background: var(--color-primary); }
|
||||
.bg-secondary { background: var(--color-secondary); }
|
||||
.bg-success { background: var(--color-success); }
|
||||
.bg-danger { background: var(--color-danger); }
|
||||
.bg-warning { background: var(--color-warning); }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-1 { margin-top: var(--spacing-xs); }
|
||||
.mt-2 { margin-top: var(--spacing-sm); }
|
||||
.mt-3 { margin-top: var(--spacing-md); }
|
||||
.mt-4 { margin-top: var(--spacing-lg); }
|
||||
.mt-5 { margin-top: var(--spacing-xl); }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: var(--spacing-xs); }
|
||||
.mb-2 { margin-bottom: var(--spacing-sm); }
|
||||
.mb-3 { margin-bottom: var(--spacing-md); }
|
||||
.mb-4 { margin-bottom: var(--spacing-lg); }
|
||||
.mb-5 { margin-bottom: var(--spacing-xl); }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-1 { padding: var(--spacing-xs); }
|
||||
.p-2 { padding: var(--spacing-sm); }
|
||||
.p-3 { padding: var(--spacing-md); }
|
||||
.p-4 { padding: var(--spacing-lg); }
|
||||
.p-5 { padding: var(--spacing-xl); }
|
||||
|
||||
.d-flex { display: flex; }
|
||||
.d-inline-flex { display: inline-flex; }
|
||||
.d-block { display: block; }
|
||||
.d-inline-block { display: inline-block; }
|
||||
.d-none { display: none; }
|
||||
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-column { flex-direction: column; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
|
||||
.justify-start { justify-content: flex-start; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-around { justify-content: space-around; }
|
||||
|
||||
.align-start { align-items: flex-start; }
|
||||
.align-center { align-items: center; }
|
||||
.align-end { align-items: flex-end; }
|
||||
.align-stretch { align-items: stretch; }
|
||||
|
||||
.gap-0 { gap: 0; }
|
||||
.gap-1 { gap: var(--spacing-xs); }
|
||||
.gap-2 { gap: var(--spacing-sm); }
|
||||
.gap-3 { gap: var(--spacing-md); }
|
||||
.gap-4 { gap: var(--spacing-lg); }
|
||||
.gap-5 { gap: var(--spacing-xl); }
|
||||
|
||||
.w-100 { width: 100%; }
|
||||
.h-100 { height: 100%; }
|
||||
.position-relative { position: relative; }
|
||||
.position-absolute { position: absolute; }
|
||||
.position-fixed { position: fixed; }
|
||||
82
src/styles/variables.css
Normal file
82
src/styles/variables.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-primary: #667eea;
|
||||
--color-primary-rgb: 102, 126, 234;
|
||||
--color-secondary: #764ba2;
|
||||
--color-secondary-rgb: 118, 75, 162;
|
||||
--color-accent: #f093fb;
|
||||
--color-accent-rgb: 240, 147, 251;
|
||||
|
||||
--color-success: #00b894;
|
||||
--color-danger: #e74c3c;
|
||||
--color-info: #14b8a6;
|
||||
--color-warning: #f39c12;
|
||||
|
||||
--color-background: #f5f7fa;
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-light: #f8fafc;
|
||||
|
||||
--color-text-primary: #2d3748;
|
||||
--color-text-secondary: #4a5568;
|
||||
--color-text-light: #718096;
|
||||
--color-text-white: #ffffff;
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 12px;
|
||||
--spacing-lg: 16px;
|
||||
--spacing-xl: 24px;
|
||||
--spacing-2xl: 32px;
|
||||
--spacing-3xl: 48px;
|
||||
--spacing-4xl: 64px;
|
||||
|
||||
/* Typography */
|
||||
--font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-secondary: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 24px;
|
||||
--font-size-3xl: 32px;
|
||||
--font-size-4xl: 48px;
|
||||
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
|
||||
/* Border Radius */
|
||||
--border-radius-sm: 4px;
|
||||
--border-radius-md: 8px;
|
||||
--border-radius-lg: 12px;
|
||||
--border-radius-xl: 16px;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-small: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-large: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms;
|
||||
--transition-normal: 300ms;
|
||||
--transition-slow: 500ms;
|
||||
|
||||
--transition-color: color var(--transition-normal) ease;
|
||||
--transition-background: background var(--transition-normal) ease;
|
||||
--transition-border: border-color var(--transition-normal) ease;
|
||||
--transition-box-shadow: box-shadow var(--transition-normal) ease;
|
||||
|
||||
/* Layout */
|
||||
--header-height: 64px;
|
||||
--sidebar-width: 240px;
|
||||
--content-max-width: 1200px;
|
||||
}
|
||||
1
src/views/.gitkeep
Normal file
1
src/views/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Views directory
|
||||
1324
src/views/Admin.vue
Normal file
1324
src/views/Admin.vue
Normal file
File diff suppressed because it is too large
Load Diff
412
src/views/AdminLayout.vue
Normal file
412
src/views/AdminLayout.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<div class="admin-layout">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<h1>抽奖管理系统</h1>
|
||||
<div style="display: flex; gap: 10px">
|
||||
<el-button type="info" @click="openDisplaySettings">
|
||||
<el-icon><Setting /></el-icon>
|
||||
显示设置
|
||||
</el-button>
|
||||
<el-button type="info" @click="showBackgroundDialog = true">
|
||||
<el-icon><Picture /></el-icon>
|
||||
背景设置
|
||||
</el-button>
|
||||
<el-button type="info" @click="showShortcutGuide = true">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
快捷键指南
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-container>
|
||||
<el-aside width="200px">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
router
|
||||
class="admin-menu"
|
||||
>
|
||||
<el-menu-item index="/admin/participants">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>名单管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/prizes">
|
||||
<el-icon><Present /></el-icon>
|
||||
<span>奖品管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/rounds">
|
||||
<el-icon><List /></el-icon>
|
||||
<span>轮次管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/winners">
|
||||
<el-icon><Trophy /></el-icon>
|
||||
<span>中奖记录</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-main>
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 快捷键指南对话框 -->
|
||||
<el-dialog v-model="showShortcutGuide" title="大屏端快捷键指南" width="500px">
|
||||
<div class="shortcut-guide">
|
||||
<div class="shortcut-item">
|
||||
<span class="shortcut-key">← →</span>
|
||||
<span class="shortcut-desc">切换轮次(左右方向键)</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span class="shortcut-key">Space</span>
|
||||
<span class="shortcut-desc">开始/停止抽奖</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="showShortcutGuide = false">知道了</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 背景图片配置对话框 -->
|
||||
<el-dialog v-model="showBackgroundDialog" title="大屏端背景图片" width="500px">
|
||||
<div class="background-config">
|
||||
<div v-if="store.backgroundImage" class="current-background">
|
||||
<div class="background-label">当前背景图片:</div>
|
||||
<img :src="store.backgroundImage" class="background-preview" />
|
||||
<el-button type="danger" @click="clearBackgroundImage" style="margin-top: 10px">
|
||||
清除背景
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<div class="background-label">上传新背景:</div>
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:on-change="handleBackgroundChange"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽图片到此处或 <em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 JPG、PNG、GIF 等图片格式,最大支持10MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<div v-if="backgroundImagePreview" class="preview-section">
|
||||
<div class="background-label">预览:</div>
|
||||
<img :src="backgroundImagePreview" class="background-preview" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showBackgroundDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveBackgroundImage" :disabled="!backgroundImagePreview">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 显示设置对话框 -->
|
||||
<el-dialog v-model="showDisplaySettingsDialog" title="大屏端显示设置" width="400px">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="每行显示人数">
|
||||
<el-input-number
|
||||
v-model="tempColumnsPerRow"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
/>
|
||||
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
|
||||
设置大屏端名单每行显示的人数,建议值:2-5
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showDisplaySettingsDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveDisplaySettings">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useLotteryStore } from '../store'
|
||||
import { User, Present, List, Trophy, Picture, QuestionFilled, UploadFilled, Setting } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useLotteryStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
// 初始化store
|
||||
onMounted(async () => {
|
||||
await store.initialize()
|
||||
})
|
||||
|
||||
// 快捷键指南
|
||||
const showShortcutGuide = ref(false)
|
||||
|
||||
// 显示设置
|
||||
const showDisplaySettingsDialog = ref(false)
|
||||
const tempColumnsPerRow = ref(3)
|
||||
|
||||
const openDisplaySettings = () => {
|
||||
tempColumnsPerRow.value = store.columnsPerRow
|
||||
showDisplaySettingsDialog.value = true
|
||||
}
|
||||
|
||||
const saveDisplaySettings = async () => {
|
||||
try {
|
||||
await store.setColumnsPerRow(tempColumnsPerRow.value)
|
||||
showDisplaySettingsDialog.value = false
|
||||
ElMessage.success('显示设置已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error('保存显示设置失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 背景图片配置
|
||||
const showBackgroundDialog = ref(false)
|
||||
const backgroundImageFile = ref(null)
|
||||
const backgroundImagePreview = ref('')
|
||||
|
||||
const handleBackgroundChange = (file) => {
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (file.raw.size > maxSize) {
|
||||
ElMessage.error('图片大小不能超过10MB,请使用较小的图片')
|
||||
backgroundImageFile.value = null
|
||||
backgroundImagePreview.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
backgroundImageFile.value = file.raw
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
backgroundImagePreview.value = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file.raw)
|
||||
}
|
||||
|
||||
const saveBackgroundImage = async () => {
|
||||
if (backgroundImagePreview.value) {
|
||||
try {
|
||||
await store.setBackgroundImage(backgroundImagePreview.value)
|
||||
showBackgroundDialog.value = false
|
||||
backgroundImagePreview.value = ''
|
||||
backgroundImageFile.value = null
|
||||
ElMessage.success('背景图片已设置')
|
||||
} catch (error) {
|
||||
ElMessage.error('保存背景图片失败:' + error.message)
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('请选择图片')
|
||||
}
|
||||
}
|
||||
|
||||
const clearBackgroundImage = async () => {
|
||||
try {
|
||||
await store.clearBackgroundImage()
|
||||
ElMessage.success('背景图片已清除')
|
||||
} catch (error) {
|
||||
ElMessage.error('清除背景图片失败:' + error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
min-height: 100vh;
|
||||
background: var(--color-background);
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background: var(--color-background);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--spacing-2xl);
|
||||
box-shadow: var(--shadow-small);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.el-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-header h1 {
|
||||
margin: 0;
|
||||
font-family: var(--font-family-secondary);
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.el-header .el-button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
transition: var(--transition-color), var(--transition-background);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.el-header .el-button:hover {
|
||||
background: rgba(var(--color-primary-rgb), 0.1);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
background: var(--color-background);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
.admin-menu {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.admin-menu .el-menu-item {
|
||||
margin: var(--spacing-sm) var(--spacing-lg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.admin-menu .el-menu-item:hover {
|
||||
background: rgba(var(--color-primary-rgb), 0.1);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.admin-menu .el-menu-item.is-active {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-white);
|
||||
}
|
||||
|
||||
.admin-menu .el-menu-item .el-icon {
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: var(--spacing-3xl);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 快捷键指南样式 */
|
||||
.shortcut-guide {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-3xl);
|
||||
padding: var(--spacing-2xl) 0;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xl);
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text-white);
|
||||
font-family: var(--font-family-secondary);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-3xl);
|
||||
margin-bottom: var(--spacing-md);
|
||||
box-shadow: 0 4px 12px rgba(var(--color-secondary-rgb), 0.3);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shortcut-desc {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-base);
|
||||
text-align: center;
|
||||
line-height: var(--line-height-relaxed);
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
/* 背景图片配置样式 */
|
||||
.background-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.current-background {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-border-light);
|
||||
border-radius: var(--border-radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-border-light);
|
||||
border-radius: var(--border-radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.background-label {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.background-preview {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
max-height: 250px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-top: var(--spacing-lg);
|
||||
padding-top: var(--spacing-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
454
src/views/Display.vue
Normal file
454
src/views/Display.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div
|
||||
class="display-container"
|
||||
@keydown="handleKeydown"
|
||||
tabindex="0"
|
||||
:style="{ backgroundImage: store.backgroundImage ? `url(${store.backgroundImage})` : 'none' }"
|
||||
>
|
||||
<!-- 快捷键提示 -->
|
||||
<div class="shortcut-hints" v-if="!store.isRolling">
|
||||
<div class="hint-item">
|
||||
<span class="key">← →</span>
|
||||
<span class="desc">切换轮次</span>
|
||||
</div>
|
||||
<div class="hint-item">
|
||||
<span class="key">Space</span>
|
||||
<span class="desc">开始/停止</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前轮次信息 -->
|
||||
<div class="current-round-info" v-if="selectedRound">
|
||||
<div class="round-name">{{ selectedRound.name }}</div>
|
||||
<div class="prize-name">{{ getPrizeName(selectedRound.prizeId) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动模式 -->
|
||||
<div v-if="store.displayMode === 'scroll'" class="scroll-mode">
|
||||
<div class="scroll-item" :class="{ 'highlight': true }">
|
||||
{{ getParticipantName(displayList[highlightIndex]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 抽奖结果模式 -->
|
||||
<div v-else class="result-mode">
|
||||
<div class="winners-list" :style="{ 'grid-template-columns': `repeat(${store.columnsPerRow}, 1fr)` }">
|
||||
<div
|
||||
v-for="(winner, index) in currentWinners"
|
||||
:key="winner.id || index"
|
||||
class="winner-item"
|
||||
:style="{ animationDelay: `${index * 0.2}s` }"
|
||||
>
|
||||
<div class="winner-name">{{ getParticipantName(winner.participant) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态指示器 -->
|
||||
<div class="status-indicator" :class="{ 'active': store.isRolling }">
|
||||
{{ store.isRolling ? '抽奖中...' : '等待抽奖' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useLotteryStore } from '../store'
|
||||
|
||||
const store = useLotteryStore()
|
||||
|
||||
const selectedRound = ref(null)
|
||||
const highlightIndex = ref(0)
|
||||
let highlightInterval = null
|
||||
|
||||
const displayList = computed(() => {
|
||||
if (store.participants.length === 0) return ['暂无名单']
|
||||
return store.participants
|
||||
})
|
||||
|
||||
const getPrizeName = (prizeId) => {
|
||||
const prize = store.prizes.find(p => p.id === prizeId)
|
||||
return prize ? prize.name : ''
|
||||
}
|
||||
|
||||
const getParticipantName = (person) => {
|
||||
if (!person) return '无数据'
|
||||
if (typeof person === 'string') return person
|
||||
|
||||
// 如果是旧的中奖记录(只有 name 字段),直接返回 name
|
||||
if (person.name && !person.participant && Object.keys(person).length === 1) {
|
||||
return person.name
|
||||
}
|
||||
|
||||
// 收集所有配置字段的值
|
||||
const displayParts = []
|
||||
|
||||
store.fields.forEach(field => {
|
||||
const value = person[field.key]
|
||||
if (value && value.trim()) {
|
||||
displayParts.push(`${field.label}: ${value}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有配置字段的值,返回所有字段
|
||||
if (displayParts.length > 0) {
|
||||
return displayParts.join(' | ')
|
||||
}
|
||||
|
||||
// 如果配置的字段都没有值,尝试从参与者对象的所有字段中找
|
||||
const allKeys = Object.keys(person).filter(key => key !== 'id' && key !== 'name')
|
||||
if (allKeys.length > 0) {
|
||||
// 优先找包含 "name" 的字段
|
||||
const nameKey = allKeys.find(key => key.toLowerCase().includes('name'))
|
||||
if (nameKey && person[nameKey]) {
|
||||
return `${nameKey}: ${person[nameKey]}`
|
||||
}
|
||||
// 否则返回第一个字段的值
|
||||
const firstKey = allKeys[0]
|
||||
return `${firstKey}: ${person[firstKey]}`
|
||||
}
|
||||
|
||||
return '无数据'
|
||||
}
|
||||
|
||||
const getParticipantDetails = (person) => {
|
||||
// 已经在 getParticipantName 中显示所有字段,这里返回空
|
||||
return ''
|
||||
}
|
||||
|
||||
const currentWinners = computed(() => {
|
||||
if (!selectedRound.value) return []
|
||||
return store.winners
|
||||
.filter(w => w.roundId === selectedRound.value.id)
|
||||
.sort((a, b) => new Date(a.time) - new Date(b.time))
|
||||
})
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
// 空格键:开始/停止抽奖
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault()
|
||||
if (store.isRolling) {
|
||||
handleStopLottery()
|
||||
} else if (selectedRound.value && !selectedRound.value.completed) {
|
||||
handleStartLottery()
|
||||
}
|
||||
}
|
||||
// 左右方向键:切换轮次
|
||||
else if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') {
|
||||
e.preventDefault()
|
||||
if (store.rounds.length === 0) return
|
||||
|
||||
let currentIndex = -1
|
||||
if (selectedRound.value) {
|
||||
currentIndex = store.rounds.findIndex(r => r.id === selectedRound.value.id)
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowLeft') {
|
||||
// 向左切换
|
||||
currentIndex = currentIndex <= 0 ? store.rounds.length - 1 : currentIndex - 1
|
||||
} else {
|
||||
// 向右切换
|
||||
currentIndex = currentIndex >= store.rounds.length - 1 ? 0 : currentIndex + 1
|
||||
}
|
||||
|
||||
selectRound(store.rounds[currentIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const selectRound = (round) => {
|
||||
selectedRound.value = round
|
||||
}
|
||||
|
||||
const handleStartLottery = () => {
|
||||
if (!selectedRound.value) return
|
||||
try {
|
||||
store.startLottery(selectedRound.value)
|
||||
startScroll()
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopLottery = () => {
|
||||
store.stopLottery()
|
||||
stopScroll()
|
||||
}
|
||||
|
||||
const startScroll = () => {
|
||||
if (highlightInterval) clearInterval(highlightInterval)
|
||||
|
||||
highlightInterval = setInterval(() => {
|
||||
highlightIndex.value = (highlightIndex.value + 1) % displayList.value.length
|
||||
}, 80)
|
||||
}
|
||||
|
||||
const stopScroll = () => {
|
||||
if (highlightInterval) {
|
||||
clearInterval(highlightInterval)
|
||||
highlightInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await store.initialize()
|
||||
|
||||
// 自动聚焦以便接收键盘事件
|
||||
document.querySelector('.display-container')?.focus()
|
||||
|
||||
// 自动选择第一个未完成的轮次
|
||||
if (store.rounds.length > 0) {
|
||||
const firstIncompleteRound = store.rounds.find(r => !r.completed)
|
||||
if (firstIncompleteRound) {
|
||||
selectedRound.value = firstIncompleteRound
|
||||
} else {
|
||||
selectedRound.value = store.rounds[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (store.displayMode === 'scroll' && store.isRolling) {
|
||||
startScroll()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopScroll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.display-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
outline: none;
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.display-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.display-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.display-container > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 快捷键提示 */
|
||||
.shortcut-hints {
|
||||
position: fixed;
|
||||
top: var(--spacing-xl);
|
||||
left: var(--spacing-xl);
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.hint-item {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.key {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text-white);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-sm);
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(var(--color-secondary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--color-text-white);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 当前轮次信息 */
|
||||
.current-round-info {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-4xl);
|
||||
}
|
||||
|
||||
.round-name {
|
||||
font-size: var(--font-size-4xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-white);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
||||
font-family: var(--font-family-secondary);
|
||||
}
|
||||
|
||||
.prize-name {
|
||||
font-size: 64px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-secondary);
|
||||
text-shadow: 0 0 30px rgba(var(--color-secondary-rgb), 0.8);
|
||||
font-family: var(--font-family-secondary);
|
||||
}
|
||||
|
||||
/* 滚动模式 */
|
||||
.scroll-mode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0 var(--spacing-4xl);
|
||||
}
|
||||
|
||||
.scroll-item {
|
||||
font-size: 64px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-white);
|
||||
padding: var(--spacing-2xl) var(--spacing-4xl);
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
transition: var(--transition-normal);
|
||||
font-family: var(--font-family-secondary);
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
background: rgba(var(--color-secondary-rgb), 0.2);
|
||||
border-radius: var(--border-radius-xl);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(var(--color-secondary-rgb), 0.5);
|
||||
text-shadow: 0 0 30px rgba(var(--color-secondary-rgb), 0.8);
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.scroll-item.highlight {
|
||||
transform: scale(1.05);
|
||||
background: rgba(var(--color-secondary-rgb), 0.4);
|
||||
border-color: rgba(var(--color-secondary-rgb), 0.8);
|
||||
}
|
||||
|
||||
/* 结果模式 */
|
||||
.result-mode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 var(--spacing-4xl);
|
||||
}
|
||||
|
||||
.winners-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--spacing-2xl);
|
||||
width: 100%;
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.winner-item {
|
||||
font-size: 32px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-white);
|
||||
padding: var(--spacing-xl) var(--spacing-2xl);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: var(--border-radius-xl);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
animation: fadeInUp 0.6s ease forwards;
|
||||
opacity: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-family: var(--font-family-secondary);
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 状态指示器 */
|
||||
.status-indicator {
|
||||
position: fixed;
|
||||
bottom: var(--spacing-4xl);
|
||||
right: var(--spacing-4xl);
|
||||
padding: var(--spacing-lg) var(--spacing-3xl);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50px;
|
||||
color: var(--color-text-white);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: var(--transition-normal);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-family: var(--font-family-secondary);
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background: rgba(var(--color-secondary-rgb), 0.3);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
452
src/views/admin/Participants.vue
Normal file
452
src/views/admin/Participants.vue
Normal file
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<el-card class="card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>名单管理</span>
|
||||
<div>
|
||||
<el-button type="info" size="small" @click="showFieldConfig">
|
||||
字段配置
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="showImportDialog = true">
|
||||
导入
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="exportParticipants">
|
||||
导出
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="clearParticipants">
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 单个添加表单 -->
|
||||
<div class="single-add-form">
|
||||
<el-form :model="newParticipantData" label-width="80px" size="small">
|
||||
<el-form-item
|
||||
v-for="field in store.fields"
|
||||
:key="field.id"
|
||||
:label="field.label"
|
||||
:required="field.required"
|
||||
>
|
||||
<el-input
|
||||
v-model="newParticipantData[field.key]"
|
||||
:placeholder="`输入${field.label}`"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="addParticipant" style="width: 100%">
|
||||
添加
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 参与者表格 -->
|
||||
<div class="participant-table">
|
||||
<el-table :data="store.participants" style="width: 100%" stripe max-height="400">
|
||||
<el-table-column
|
||||
v-for="field in store.fields"
|
||||
:key="field.id"
|
||||
:prop="field.key"
|
||||
:label="field.label"
|
||||
:min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="操作" fixed="right" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showParticipantDetail(row)"
|
||||
v-if="store.fields.length > 1"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeParticipant(row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="participant-count">
|
||||
共 {{ store.participants.length }} 人
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 字段配置对话框 -->
|
||||
<el-dialog v-model="showFieldDialog" title="字段配置" width="600px">
|
||||
<div class="field-config">
|
||||
<div class="field-list">
|
||||
<div v-for="field in tempFields" :key="field.id" class="field-item">
|
||||
<el-input v-model="field.key" placeholder="字段键" size="small" style="width: 120px" />
|
||||
<el-input v-model="field.label" placeholder="字段名称" size="small" style="width: 120px" />
|
||||
<el-checkbox v-model="field.required">必填</el-checkbox>
|
||||
<el-button type="danger" size="small" @click="removeField(field.id)" :disabled="tempFields.length <= 1">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="addField" style="margin-top: 15px">添加字段</el-button>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showFieldDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveFields">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<el-dialog v-model="showImportDialog" title="导入名单" width="500px">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".csv"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽文件到此处或 <em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 .csv 格式文件,第一行为字段名,后续行为数据
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button @click="showImportDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleImport" :disabled="!selectedFile">导入</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useLotteryStore } from '../../store'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const store = useLotteryStore()
|
||||
|
||||
// 初始化store
|
||||
onMounted(async () => {
|
||||
await store.initialize()
|
||||
})
|
||||
|
||||
// 字段配置
|
||||
const showFieldDialog = ref(false)
|
||||
const tempFields = ref([])
|
||||
|
||||
const showFieldConfig = () => {
|
||||
tempFields.value = JSON.parse(JSON.stringify(store.fields))
|
||||
showFieldDialog.value = true
|
||||
}
|
||||
|
||||
const addField = () => {
|
||||
tempFields.value.push({
|
||||
id: Date.now(),
|
||||
key: '',
|
||||
label: '',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
|
||||
const removeField = (id) => {
|
||||
const index = tempFields.value.findIndex(f => f.id === id)
|
||||
if (index > -1) {
|
||||
tempFields.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const saveFields = () => {
|
||||
const validFields = tempFields.value.filter(f => f.key && f.label)
|
||||
if (validFields.length === 0) {
|
||||
ElMessage.error('至少需要一个有效字段')
|
||||
return
|
||||
}
|
||||
|
||||
store.fields.splice(0, store.fields.length, ...validFields)
|
||||
showFieldDialog.value = false
|
||||
ElMessage.success('字段配置已保存')
|
||||
}
|
||||
|
||||
// 名单管理
|
||||
const newParticipantData = ref({})
|
||||
const showImportDialog = ref(false)
|
||||
const uploadRef = ref(null)
|
||||
const selectedFile = ref(null)
|
||||
|
||||
const initNewParticipantData = () => {
|
||||
newParticipantData.value = {}
|
||||
store.fields.forEach(field => {
|
||||
newParticipantData.value[field.key] = ''
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => store.fields, () => {
|
||||
initNewParticipantData()
|
||||
}, { deep: true })
|
||||
|
||||
initNewParticipantData()
|
||||
|
||||
const addParticipant = () => {
|
||||
const missingFields = store.fields.filter(f => f.required && !newParticipantData.value[f.key]?.trim())
|
||||
if (missingFields.length > 0) {
|
||||
ElMessage.error(`请填写必填字段:${missingFields.map(f => f.label).join('、')}`)
|
||||
return
|
||||
}
|
||||
|
||||
const hasValue = Object.values(newParticipantData.value).some(v => v && v.trim())
|
||||
if (!hasValue) {
|
||||
ElMessage.error('请至少填写一个字段')
|
||||
return
|
||||
}
|
||||
|
||||
store.addParticipant({
|
||||
id: Date.now(),
|
||||
...newParticipantData.value
|
||||
})
|
||||
|
||||
initNewParticipantData()
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
|
||||
const showParticipantDetail = (person) => {
|
||||
const details = []
|
||||
|
||||
store.fields.forEach(field => {
|
||||
details.push(`${field.label}: ${person[field.key] || '-'}`)
|
||||
})
|
||||
|
||||
const allKeys = Object.keys(person).filter(key => key !== 'id')
|
||||
const configuredKeys = store.fields.map(f => f.key)
|
||||
const unconfiguredKeys = allKeys.filter(key => !configuredKeys.includes(key))
|
||||
|
||||
if (unconfiguredKeys.length > 0) {
|
||||
details.push('')
|
||||
details.push('未配置字段:')
|
||||
unconfiguredKeys.forEach(key => {
|
||||
details.push(`${key}: ${person[key]}`)
|
||||
})
|
||||
}
|
||||
|
||||
ElMessage({
|
||||
message: details.join('\n'),
|
||||
type: 'info',
|
||||
duration: 0,
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
|
||||
const removeParticipant = (id) => {
|
||||
store.removeParticipant(id)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
|
||||
const handleFileChange = (file) => {
|
||||
selectedFile.value = file.raw
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
try {
|
||||
if (selectedFile.value) {
|
||||
const count = await store.importParticipantsFromFile(selectedFile.value)
|
||||
selectedFile.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
showImportDialog.value = false
|
||||
ElMessage.success(`成功导入 ${count} 人`)
|
||||
} else {
|
||||
ElMessage.warning('请选择文件')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('导入失败:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const exportParticipants = () => {
|
||||
store.exportParticipants()
|
||||
ElMessage.success('导出成功')
|
||||
}
|
||||
|
||||
const clearParticipants = () => {
|
||||
if (store.participants.length === 0) {
|
||||
ElMessage.warning('名单已经是空的')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
'确定要清空所有名单吗?此操作不可恢复。',
|
||||
'确认清空',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
store.clearParticipants()
|
||||
ElMessage.success('已清空名单')
|
||||
}).catch(() => {
|
||||
// 用户取消操作
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-small);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.single-add-form {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background: var(--color-border-light);
|
||||
border-radius: var(--border-radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.single-add-form :deep(.el-form-item) {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.single-add-form :deep(.el-form-item__label) {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.single-add-form :deep(.el-button) {
|
||||
margin-top: var(--spacing-sm);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.participant-table {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table) {
|
||||
border-radius: var(--border-radius-md);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table th) {
|
||||
background: var(--color-background);
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-family: var(--font-family-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table td) {
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table .el-table__row:hover > td) {
|
||||
background: rgba(var(--color-primary-rgb), 0.05);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
|
||||
background: rgba(var(--color-primary-rgb), 0.03);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table__body-wrapper) {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table__body-wrapper::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table__body-wrapper::-webkit-scrollbar-track) {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb) {
|
||||
background: var(--color-text-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.participant-table :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.participant-count {
|
||||
text-align: right;
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-sm);
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: var(--spacing-md);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.field-config {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.field-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.field-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.field-list::-webkit-scrollbar-track {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.field-list::-webkit-scrollbar-thumb {
|
||||
background: var(--color-text-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.field-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.field-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: var(--transition-border), var(--transition-box-shadow);
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.field-item:hover {
|
||||
border-color: var(--color-secondary);
|
||||
box-shadow: var(--shadow-small);
|
||||
transform: none;
|
||||
}
|
||||
</style>
|
||||
163
src/views/admin/Prizes.vue
Normal file
163
src/views/admin/Prizes.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<el-card class="card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>奖品管理</span>
|
||||
<el-button type="primary" size="small" @click="showPrizeDialog = true">
|
||||
添加奖品
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="prize-list">
|
||||
<div
|
||||
v-for="prize in store.prizes"
|
||||
:key="prize.id"
|
||||
class="prize-item"
|
||||
>
|
||||
<div class="prize-info">
|
||||
<span class="prize-name">{{ prize.name }}</span>
|
||||
<span class="prize-stock">库存: {{ prize.used }}/{{ prize.stock }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click="removePrize(prize.id)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加奖品对话框 -->
|
||||
<el-dialog v-model="showPrizeDialog" title="添加奖品" width="400px">
|
||||
<el-form :model="newPrize" label-width="80px">
|
||||
<el-form-item label="奖品名称">
|
||||
<el-input v-model="newPrize.name" placeholder="例如:一等奖" />
|
||||
</el-form-item>
|
||||
<el-form-item label="库存数量">
|
||||
<el-input-number v-model="newPrize.stock" :min="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showPrizeDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="addPrize">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useLotteryStore } from '../../store'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useLotteryStore()
|
||||
|
||||
// 初始化store
|
||||
onMounted(async () => {
|
||||
await store.initialize()
|
||||
})
|
||||
|
||||
// 奖品管理
|
||||
const showPrizeDialog = ref(false)
|
||||
const newPrize = ref({
|
||||
name: '',
|
||||
stock: 1
|
||||
})
|
||||
|
||||
const addPrize = () => {
|
||||
if (newPrize.value.name.trim()) {
|
||||
store.addPrize(newPrize.value)
|
||||
newPrize.value = { name: '', stock: 1 }
|
||||
showPrizeDialog.value = false
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
}
|
||||
|
||||
const removePrize = (id) => {
|
||||
store.removePrize(id)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-small);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.prize-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.prize-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.prize-list::-webkit-scrollbar-track {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.prize-list::-webkit-scrollbar-thumb {
|
||||
background: var(--color-text-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.prize-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.prize-item {
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: var(--transition-border), var(--transition-box-shadow);
|
||||
background: var(--color-background);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prize-item:hover {
|
||||
border-color: var(--color-secondary);
|
||||
box-shadow: var(--shadow-small);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.prize-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.prize-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-family: var(--font-family-primary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.prize-stock {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
</style>
|
||||
199
src/views/admin/Rounds.vue
Normal file
199
src/views/admin/Rounds.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<el-card class="card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>轮次管理</span>
|
||||
<el-button type="primary" size="small" @click="showRoundDialog = true">
|
||||
添加轮次
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="round-list">
|
||||
<div
|
||||
v-for="round in store.rounds"
|
||||
:key="round.id"
|
||||
class="round-item"
|
||||
:class="{ 'completed': round.completed }"
|
||||
>
|
||||
<div class="round-info">
|
||||
<span class="round-name">{{ round.name }}</span>
|
||||
<span class="round-detail">{{ getPrizeName(round.prizeId) }} × {{ round.count }}人</span>
|
||||
</div>
|
||||
<div class="round-actions">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeRound(round.id)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加轮次对话框 -->
|
||||
<el-dialog v-model="showRoundDialog" title="添加轮次" width="400px">
|
||||
<el-form :model="newRound" label-width="80px">
|
||||
<el-form-item label="轮次名称">
|
||||
<el-input v-model="newRound.name" placeholder="例如:第一轮" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择奖品">
|
||||
<el-select v-model="newRound.prizeId" placeholder="请选择奖品" style="width: 100%">
|
||||
<el-option
|
||||
v-for="prize in store.prizes"
|
||||
:key="prize.id"
|
||||
:label="`${prize.name} (剩余: ${prize.stock - prize.used})`"
|
||||
:value="prize.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="抽取人数">
|
||||
<el-input-number v-model="newRound.count" :min="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showRoundDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="addRound">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useLotteryStore } from '../../store'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useLotteryStore()
|
||||
|
||||
// 初始化store
|
||||
onMounted(async () => {
|
||||
await store.initialize()
|
||||
})
|
||||
|
||||
// 轮次管理
|
||||
const showRoundDialog = ref(false)
|
||||
const newRound = ref({
|
||||
name: '',
|
||||
prizeId: null,
|
||||
count: 1
|
||||
})
|
||||
|
||||
const getPrizeName = (prizeId) => {
|
||||
const prize = store.prizes.find(p => p.id === prizeId)
|
||||
return prize ? prize.name : ''
|
||||
}
|
||||
|
||||
const addRound = () => {
|
||||
if (newRound.value.name.trim() && newRound.value.prizeId) {
|
||||
store.addRound(newRound.value)
|
||||
newRound.value = { name: '', prizeId: null, count: 1 }
|
||||
showRoundDialog.value = false
|
||||
ElMessage.success('添加成功')
|
||||
} else {
|
||||
ElMessage.warning('请填写完整信息')
|
||||
}
|
||||
}
|
||||
|
||||
const removeRound = (id) => {
|
||||
store.removeRound(id)
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-small);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.round-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.round-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.round-list::-webkit-scrollbar-track {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.round-list::-webkit-scrollbar-thumb {
|
||||
background: var(--color-text-light);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.round-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.round-item {
|
||||
padding: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: var(--transition-border), var(--transition-box-shadow);
|
||||
background: var(--color-background);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.round-item:hover {
|
||||
border-color: var(--color-secondary);
|
||||
box-shadow: var(--shadow-small);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.round-item.completed {
|
||||
opacity: 0.6;
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.round-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.round-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-family: var(--font-family-primary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.round-detail {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.round-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.round-actions .el-button {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
99
src/views/admin/Winners.vue
Normal file
99
src/views/admin/Winners.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-card class="card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>中奖记录</span>
|
||||
<div>
|
||||
<el-button type="success" size="small" @click="exportWinners">
|
||||
导出
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="resetLottery">
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="store.winners" style="width: 100%" stripe>
|
||||
<el-table-column prop="name" label="姓名" width="150" />
|
||||
<el-table-column prop="prizeName" label="奖品" width="150" />
|
||||
<el-table-column prop="roundName" label="轮次" width="150" />
|
||||
<el-table-column prop="time" label="时间" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useLotteryStore } from '../../store'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useLotteryStore()
|
||||
|
||||
// 初始化store
|
||||
onMounted(async () => {
|
||||
await store.initialize()
|
||||
})
|
||||
|
||||
// 重置抽奖
|
||||
const resetLottery = () => {
|
||||
store.resetLottery()
|
||||
ElMessage.success('已重置')
|
||||
}
|
||||
|
||||
// 导出中奖名单
|
||||
const exportWinners = () => {
|
||||
if (store.winners.length === 0) {
|
||||
ElMessage.warning('暂无中奖记录')
|
||||
return
|
||||
}
|
||||
store.exportWinners()
|
||||
ElMessage.success('导出成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-small);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border-radius: var(--border-radius-md);
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background: var(--color-background);
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-family: var(--font-family-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
:deep(.el-table td) {
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
:deep(.el-table .el-table__row:hover > td) {
|
||||
background: rgba(var(--color-primary-rgb), 0.05);
|
||||
}
|
||||
|
||||
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
|
||||
background: rgba(var(--color-primary-rgb), 0.03);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user