You've already forked rollingDraw
1286 lines
35 KiB
Vue
1286 lines
35 KiB
Vue
<template>
|
||
<div class="admin-container">
|
||
<el-container>
|
||
<el-header>
|
||
<div class="header-content">
|
||
<h1>抽奖管理系统</h1>
|
||
<div style="display: flex; gap: 10px">
|
||
<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-main>
|
||
<el-row :gutter="20">
|
||
<!-- 左侧:名单管理 -->
|
||
<el-col :span="8">
|
||
<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>
|
||
</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-list">
|
||
<div
|
||
v-for="person in store.participants"
|
||
:key="person.id"
|
||
class="participant-item"
|
||
>
|
||
<div class="participant-card">
|
||
<div class="participant-main">
|
||
<span class="participant-name">{{ getParticipantDisplay(person) }}</span>
|
||
<el-button
|
||
type="danger"
|
||
size="small"
|
||
@click="removeParticipant(person.id)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
<div class="participant-extra" v-if="store.fields.length > 1">
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
@click="showParticipantDetail(person)"
|
||
>
|
||
详情
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="participant-count">
|
||
共 {{ store.participants.length }} 人
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 中间:奖品管理 -->
|
||
<el-col :span="8">
|
||
<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-col>
|
||
|
||
<!-- 右侧:轮次管理 -->
|
||
<el-col :span="8">
|
||
<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-col>
|
||
</el-row>
|
||
|
||
<!-- 中奖记录 -->
|
||
<el-row style="margin-top: 20px">
|
||
<el-col :span="24">
|
||
<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>
|
||
</el-col>
|
||
</el-row>
|
||
</el-main>
|
||
</el-container>
|
||
|
||
<!-- 字段配置对话框 -->
|
||
<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>
|
||
|
||
<!-- 添加奖品对话框 -->
|
||
<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>
|
||
|
||
<!-- 添加轮次对话框 -->
|
||
<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>
|
||
|
||
<!-- 快捷键指南对话框 -->
|
||
<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>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch, onMounted } from 'vue'
|
||
import { useLotteryStore } from './store'
|
||
import { Delete, UploadFilled, QuestionFilled, Picture } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
const store = useLotteryStore()
|
||
|
||
// 初始化store
|
||
onMounted(async () => {
|
||
await store.initialize()
|
||
})
|
||
|
||
// 快捷键指南
|
||
const showShortcutGuide = ref(false)
|
||
|
||
// 背景图片配置
|
||
const showBackgroundDialog = ref(false)
|
||
const backgroundImageFile = ref(null)
|
||
const backgroundImagePreview = ref('')
|
||
|
||
const handleBackgroundChange = (file) => {
|
||
// 检查文件大小(限制为10MB)
|
||
const maxSize = 10 * 1024 * 1024 // 10MB
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 字段配置
|
||
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 getParticipantDisplay = (person) => {
|
||
// 首先尝试从配置的字段中查找
|
||
const nameField = store.fields.find(f => f.key === 'name')
|
||
if (nameField && person[nameField.key]) {
|
||
return person[nameField.key]
|
||
}
|
||
|
||
// 如果没有 name 字段,尝试从配置的字段中找第一个有值的
|
||
const firstFieldWithValue = store.fields.find(f => person[f.key])
|
||
if (firstFieldWithValue) {
|
||
return person[firstFieldWithValue.key]
|
||
}
|
||
|
||
// 如果配置的字段都没有值,尝试从参与者对象的所有字段中找
|
||
const allKeys = Object.keys(person).filter(key => key !== 'id')
|
||
if (allKeys.length > 0) {
|
||
// 优先找包含 "name" 的字段
|
||
const nameKey = allKeys.find(key => key.toLowerCase().includes('name'))
|
||
if (nameKey) {
|
||
return person[nameKey]
|
||
}
|
||
// 否则返回第一个字段的值
|
||
return person[allKeys[0]]
|
||
}
|
||
|
||
return JSON.stringify(person)
|
||
}
|
||
|
||
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 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('删除成功')
|
||
}
|
||
|
||
// 轮次管理
|
||
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('删除成功')
|
||
}
|
||
|
||
// 重置抽奖
|
||
const resetLottery = () => {
|
||
store.resetLottery()
|
||
ElMessage.success('已重置')
|
||
}
|
||
|
||
// 导出中奖名单
|
||
const exportWinners = () => {
|
||
if (store.winners.length === 0) {
|
||
ElMessage.warning('暂无中奖记录')
|
||
return
|
||
}
|
||
store.exportWinners()
|
||
ElMessage.success('导出成功')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.admin-container {
|
||
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%;
|
||
max-width: 1400px;
|
||
}
|
||
|
||
.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-main {
|
||
padding: var(--spacing-3xl);
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.card {
|
||
height: 100%;
|
||
margin-bottom: var(--spacing-2xl);
|
||
border-radius: var(--border-radius-lg);
|
||
box-shadow: var(--shadow-small);
|
||
transition: var(--transition-box-shadow);
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
.card:hover {
|
||
box-shadow: var(--shadow-medium);
|
||
transform: none;
|
||
}
|
||
|
||
.shortcut-card {
|
||
background: var(--color-primary);
|
||
}
|
||
|
||
.shortcut-card :deep(.el-card__header) {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.shortcut-card :deep(.el-card__header span) {
|
||
color: var(--color-text-white);
|
||
font-weight: var(--font-weight-bold);
|
||
font-family: var(--font-family-secondary);
|
||
}
|
||
|
||
.shortcut-card :deep(.el-card__body) {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.participant-list {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.participant-item {
|
||
margin: var(--spacing-md) 0;
|
||
}
|
||
|
||
.participant-card {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: var(--spacing-lg);
|
||
background: var(--color-border-light);
|
||
border-radius: var(--border-radius-md);
|
||
transition: var(--transition-background), var(--transition-border), var(--transition-box-shadow);
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.participant-card:hover {
|
||
background: var(--color-background);
|
||
border-color: var(--color-secondary);
|
||
box-shadow: var(--shadow-small);
|
||
transform: none;
|
||
}
|
||
|
||
.participant-card:hover .participant-extra :deep(.el-button--danger) {
|
||
background: var(--color-danger);
|
||
color: var(--color-text-white);
|
||
border-color: var(--color-danger);
|
||
}
|
||
|
||
.participant-card:hover .participant-extra :deep(.el-button--primary) {
|
||
background: var(--color-primary);
|
||
color: var(--color-text-white);
|
||
border-color: var(--color-primary);
|
||
}
|
||
|
||
.participant-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-md);
|
||
flex: 1;
|
||
}
|
||
|
||
.participant-name {
|
||
font-size: var(--font-size-base);
|
||
font-weight: var(--font-weight-medium);
|
||
color: var(--color-text-primary);
|
||
font-family: var(--font-family-primary);
|
||
flex: 1;
|
||
}
|
||
|
||
.participant-extra {
|
||
display: flex;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.participant-extra :deep(.el-button) {
|
||
min-width: 60px;
|
||
}
|
||
|
||
.participant-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.participant-list::-webkit-scrollbar-track {
|
||
background: var(--color-border);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.participant-list::-webkit-scrollbar-thumb {
|
||
background: var(--color-text-light);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.participant-list::-webkit-scrollbar-thumb:hover {
|
||
background: var(--color-text-secondary);
|
||
}
|
||
|
||
.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-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);
|
||
}
|
||
|
||
.prize-list,
|
||
.round-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.prize-list::-webkit-scrollbar,
|
||
.round-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.prize-list::-webkit-scrollbar-track,
|
||
.round-list::-webkit-scrollbar-track {
|
||
background: var(--color-border);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.prize-list::-webkit-scrollbar-thumb,
|
||
.round-list::-webkit-scrollbar-thumb {
|
||
background: var(--color-text-light);
|
||
border-radius: var(--border-radius-sm);
|
||
}
|
||
|
||
.prize-list::-webkit-scrollbar-thumb:hover,
|
||
.round-list::-webkit-scrollbar-thumb:hover {
|
||
background: var(--color-text-secondary);
|
||
}
|
||
|
||
.prize-item,
|
||
.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);
|
||
}
|
||
|
||
.prize-item:hover,
|
||
.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);
|
||
}
|
||
|
||
.prize-info,
|
||
.round-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.prize-name,
|
||
.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);
|
||
}
|
||
|
||
.prize-stock,
|
||
.round-detail {
|
||
color: var(--color-text-light);
|
||
font-size: var(--font-size-sm);
|
||
font-family: var(--font-family-primary);
|
||
}
|
||
|
||
.prize-actions,
|
||
.round-actions {
|
||
display: flex;
|
||
gap: var(--spacing-md);
|
||
margin-top: var(--spacing-md);
|
||
}
|
||
|
||
.prize-actions .el-button,
|
||
.round-actions .el-button {
|
||
flex: 1;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 表格样式优化 */
|
||
: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);
|
||
}
|
||
|
||
/* 对话框样式优化 */
|
||
:deep(.el-dialog) {
|
||
border-radius: var(--border-radius-lg);
|
||
box-shadow: var(--shadow-large);
|
||
}
|
||
|
||
:deep(.el-dialog__header) {
|
||
background: var(--color-primary);
|
||
color: var(--color-text-white);
|
||
padding: var(--spacing-xl) var(--spacing-2xl);
|
||
}
|
||
|
||
:deep(.el-dialog__title) {
|
||
font-family: var(--font-family-secondary);
|
||
font-weight: var(--font-weight-semibold);
|
||
font-size: var(--font-size-lg);
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding: var(--spacing-2xl);
|
||
}
|
||
|
||
:deep(.el-dialog__footer) {
|
||
padding: var(--spacing-lg) var(--spacing-2xl);
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
/* 按钮样式优化 */
|
||
:deep(.el-button--primary) {
|
||
background: var(--color-primary);
|
||
border-color: var(--color-primary);
|
||
color: var(--color-text-white);
|
||
font-family: var(--font-family-primary);
|
||
font-weight: var(--font-weight-medium);
|
||
}
|
||
|
||
:deep(.el-button--primary:hover) {
|
||
background: var(--color-accent);
|
||
border-color: var(--color-accent);
|
||
transform: none;
|
||
}
|
||
|
||
:deep(.el-button--success) {
|
||
background: var(--color-success);
|
||
border-color: var(--color-success);
|
||
color: var(--color-text-white);
|
||
font-family: var(--font-family-primary);
|
||
font-weight: var(--font-weight-medium);
|
||
}
|
||
|
||
:deep(.el-button--success:hover) {
|
||
background: rgb(0, 148, 127);
|
||
border-color: rgb(0, 148, 127);
|
||
transform: none;
|
||
}
|
||
|
||
:deep(.el-button--danger) {
|
||
background: var(--color-danger);
|
||
border-color: var(--color-danger);
|
||
color: var(--color-text-white);
|
||
font-family: var(--font-family-primary);
|
||
font-weight: var(--font-weight-medium);
|
||
}
|
||
|
||
:deep(.el-button--danger:hover) {
|
||
background: rgb(197, 48, 63);
|
||
border-color: rgb(197, 48, 63);
|
||
transform: none;
|
||
}
|
||
|
||
:deep(.el-button--info) {
|
||
background: var(--color-info);
|
||
border-color: var(--color-info);
|
||
color: var(--color-text-white);
|
||
font-family: var(--font-family-primary);
|
||
font-weight: var(--font-weight-medium);
|
||
}
|
||
|
||
:deep(.el-button--info:hover) {
|
||
background: rgb(20, 146, 165);
|
||
border-color: rgb(20, 146, 165);
|
||
transform: none;
|
||
}
|
||
|
||
/* 表单样式优化 */
|
||
:deep(.el-form-item__label) {
|
||
font-family: var(--font-family-primary);
|
||
font-weight: var(--font-weight-medium);
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
:deep(.el-input__wrapper) {
|
||
border-radius: var(--border-radius-md);
|
||
border: 1px solid var(--color-border);
|
||
background: var(--color-background);
|
||
transition: var(--transition-border), var(--transition-box-shadow);
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.el-input__wrapper:hover) {
|
||
border-color: var(--color-primary);
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.el-input__wrapper.is-focus) {
|
||
border-color: var(--color-primary);
|
||
box-shadow: var(--shadow-small);
|
||
}
|
||
|
||
:deep(.el-select .el-input__wrapper) {
|
||
border-radius: var(--border-radius-md);
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.el-textarea__inner) {
|
||
border-radius: var(--border-radius-md);
|
||
border: 1px solid var(--color-border);
|
||
background: var(--color-background);
|
||
transition: var(--transition-border), var(--transition-box-shadow);
|
||
font-family: var(--font-family-primary);
|
||
font-size: var(--font-size-base);
|
||
color: var(--color-text-primary);
|
||
line-height: var(--line-height-relaxed);
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.el-textarea__inner:hover) {
|
||
border-color: var(--color-primary);
|
||
box-shadow: none;
|
||
}
|
||
|
||
:deep(.el-textarea__inner:focus) {
|
||
border-color: var(--color-primary);
|
||
box-shadow: var(--shadow-small);
|
||
}
|
||
|
||
:deep(.el-input__inner::placeholder),
|
||
:deep(.el-textarea__inner::placeholder) {
|
||
color: var(--color-text-light);
|
||
}
|
||
|
||
/* 上传组件样式优化 */
|
||
:deep(.el-upload-dragger) {
|
||
border-radius: var(--border-radius-lg);
|
||
border: 2px dashed var(--color-border);
|
||
transition: var(--transition-border), var(--transition-background);
|
||
background: var(--color-background);
|
||
}
|
||
|
||
:deep(.el-upload-dragger:hover) {
|
||
border-color: var(--color-primary);
|
||
background: rgba(var(--color-primary-rgb), 0.05);
|
||
}
|
||
|
||
/* 背景图片配置样式 */
|
||
.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> |