新增: 新增大屏端显示设置和性能优化

This commit is contained in:
yuantao
2026-01-16 17:45:50 +08:00
parent 466b8408ab
commit 9475f04737
10 changed files with 1938 additions and 740 deletions

View File

@@ -14,6 +14,13 @@ export const useLotteryStore = defineStore('lottery', () => {
const displayMode = ref('scroll')
const backgroundImage = ref('')
const columnsPerRow = ref(3) // 每行显示的人数
const displayFontColor = ref('#FFFFFF') // 大屏端字体颜色
const prizeNameFontColor = ref('#00B4D8') // 奖品名称字体颜色
const roundNameFontSize = ref(36) // 轮次名称字体大小
const prizeNameFontSize = ref(64) // 奖品名称字体大小
const participantFontSize = ref(32) // 人名字体大小
const displayFields = ref([]) // 大屏端显示的字段列表
const showFieldLabels = ref(true) // 是否显示字段标签
const isInitialized = ref(false)
// 从IndexedDB初始化数据
@@ -35,6 +42,13 @@ export const useLotteryStore = defineStore('lottery', () => {
displayMode.value = data.lottery_displayMode || 'scroll'
backgroundImage.value = data.lottery_backgroundImage || ''
columnsPerRow.value = data.lottery_columnsPerRow || 3
displayFontColor.value = data.lottery_displayFontColor || '#FFFFFF'
prizeNameFontColor.value = data.lottery_prizeNameFontColor || '#00B4D8'
roundNameFontSize.value = data.lottery_roundNameFontSize || 36
prizeNameFontSize.value = data.lottery_prizeNameFontSize || 64
participantFontSize.value = data.lottery_participantFontSize || 32
displayFields.value = data.lottery_displayFields || []
showFieldLabels.value = data.lottery_showFieldLabels !== undefined ? data.lottery_showFieldLabels : true
console.log('Initialized background image:', backgroundImage.value ? 'Yes' : 'No')
console.log('Initialization completed')
@@ -392,10 +406,18 @@ export const useLotteryStore = defineStore('lottery', () => {
}
const exportWinners = () => {
const headers = ['姓名', '奖品', '轮次', '时间']
const rows = winners.value.map(w => [w.name, w.prizeName, w.roundName, w.time])
// 动态生成表头
const headers = fields.value.map(f => f.label)
headers.push('奖品', '轮次', '时间')
const csv = [headers.join(','), ...rows].join('\n')
// 动态生成数据行
const rows = winners.value.map(w => {
const participantFields = fields.value.map(f => w.participant ? (w.participant[f.key] || '') : '')
participantFields.push(w.prizeName, w.roundName, w.time)
return participantFields
})
const csv = [headers.join(','), ...rows.map(row => row.join(','))].join('\n')
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
@@ -438,6 +460,41 @@ export const useLotteryStore = defineStore('lottery', () => {
}
}
const setDisplayFontColor = async (color) => {
displayFontColor.value = color
await saveData('lottery_displayFontColor', displayFontColor.value)
}
const setPrizeNameFontColor = async (color) => {
prizeNameFontColor.value = color
await saveData('lottery_prizeNameFontColor', prizeNameFontColor.value)
}
const setRoundNameFontSize = async (size) => {
roundNameFontSize.value = size
await saveData('lottery_roundNameFontSize', roundNameFontSize.value)
}
const setPrizeNameFontSize = async (size) => {
prizeNameFontSize.value = size
await saveData('lottery_prizeNameFontSize', prizeNameFontSize.value)
}
const setParticipantFontSize = async (size) => {
participantFontSize.value = size
await saveData('lottery_participantFontSize', participantFontSize.value)
}
const setDisplayFields = async (fields) => {
displayFields.value = fields
await saveData('lottery_displayFields', displayFields.value)
}
const setShowFieldLabels = async (show) => {
showFieldLabels.value = show
await saveData('lottery_showFieldLabels', showFieldLabels.value)
}
return {
// 状态
fields,
@@ -450,6 +507,13 @@ export const useLotteryStore = defineStore('lottery', () => {
displayMode,
backgroundImage,
columnsPerRow,
displayFontColor,
prizeNameFontColor,
roundNameFontSize,
prizeNameFontSize,
participantFontSize,
displayFields,
showFieldLabels,
isInitialized,
// 初始化
@@ -491,6 +555,13 @@ export const useLotteryStore = defineStore('lottery', () => {
switchDisplayMode,
setColumnsPerRow,
setBackgroundImage,
clearBackgroundImage
clearBackgroundImage,
setDisplayFontColor,
setPrizeNameFontColor,
setRoundNameFontSize,
setPrizeNameFontSize,
setParticipantFontSize,
setDisplayFields,
setShowFieldLabels
}
})

View File

@@ -1,22 +1,22 @@
/* Element Plus Custom Styles */
/* 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-primary-light-3: rgba(var(--color-primary-rgb), 0.3);
--el-color-primary-light-5: rgba(var(--color-primary-rgb), 0.5);
--el-color-primary-light-7: rgba(var(--color-primary-rgb), 0.7);
--el-color-primary-light-8: rgba(var(--color-primary-rgb), 0.8);
--el-color-primary-light-9: rgba(var(--color-primary-rgb), 0.9);
--el-color-primary-dark-2: var(--color-primary-dark);
--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-base: var(--border-radius-sm);
--el-border-radius-small: 4px;
--el-border-radius-round: 20px;
--el-border-radius-circle: 100%;
@@ -32,54 +32,74 @@
.el-button {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-medium);
border-radius: var(--border-radius-md);
border-radius: var(--border-radius-sm);
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:hover {
transform: none;
box-shadow: none;
opacity: 0.9;
}
.el-button:active {
transform: none;
}
.el-button--primary {
background: var(--color-info);
color: var(--color-text-white);
}
.el-button--primary:hover {
background: linear-gradient(135deg, var(--color-primary-dark-2) 0%, var(--color-secondary) 100%);
background: #1565C0;
}
.el-button--success {
background: var(--color-success);
border: none;
color: var(--color-text-white);
}
.el-button--success:hover {
background: #27AE60;
}
.el-button--danger {
background: var(--color-danger);
border: none;
color: var(--color-text-white);
}
.el-button--danger:hover {
background: #C0392B;
}
.el-button--warning {
background: var(--color-warning);
border: none;
color: var(--color-text-white);
}
.el-button--warning:hover {
background: #F39C12;
}
.el-button--info {
background: var(--color-info);
border: none;
background: var(--color-background-tertiary);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.el-button--info:hover {
background: #E9ECEF;
}
/* Element Plus Card Customization */
.el-card {
border-radius: var(--border-radius-lg);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
transition: all var(--transition-normal);
border: 1px solid var(--color-border);
border: none;
background: var(--color-background);
}
.el-card:hover {
@@ -89,9 +109,10 @@
.el-card__header {
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-lg) var(--spacing-xl);
font-family: var(--font-family-secondary);
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
color: var(--color-text-primary);
}
.el-card__body {
@@ -100,19 +121,19 @@
/* Element Plus Dialog Customization */
.el-dialog {
border-radius: var(--border-radius-lg);
border-radius: var(--border-radius-md);
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);
background: var(--color-background);
color: var(--color-text-primary);
padding: var(--spacing-xl) var(--spacing-2xl);
border-radius: var(--border-radius-lg) var(--border-radius-lg) 0 0;
border-bottom: 1px solid var(--color-border);
}
.el-dialog__title {
font-family: var(--font-family-secondary);
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
}
@@ -128,10 +149,11 @@
/* Element Plus Input Customization */
.el-input__wrapper {
border-radius: var(--border-radius-md);
border-radius: var(--border-radius-sm);
box-shadow: none;
border: 1px solid var(--color-border);
transition: all var(--transition-normal);
background: var(--color-background);
}
.el-input__wrapper:hover {
@@ -151,7 +173,7 @@
/* Element Plus Select Customization */
.el-select .el-input__wrapper {
border-radius: var(--border-radius-md);
border-radius: var(--border-radius-sm);
}
.el-select-dropdown__item {
@@ -186,12 +208,12 @@
}
.el-table th {
background: var(--color-background);
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-weight: var(--font-weight-semibold);
font-family: var(--font-family-secondary);
font-family: var(--font-family-primary);
font-size: var(--font-size-base);
border-bottom: 2px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.el-table td {
@@ -201,11 +223,11 @@
}
.el-table .el-table__row:hover > td {
background: rgba(var(--color-primary-rgb), 0.05);
background: var(--color-background-tertiary);
}
.el-table--striped .el-table__body tr.el-table__row--striped td {
background: rgba(var(--color-primary-rgb), 0.03);
background: var(--color-background);
}
.el-table__body tr.current-row > td {
@@ -214,7 +236,7 @@
/* Element Plus Upload Customization */
.el-upload-dragger {
border-radius: var(--border-radius-lg);
border-radius: var(--border-radius-md);
border: 2px dashed var(--color-border);
transition: all var(--transition-normal);
background: var(--color-background);
@@ -233,7 +255,7 @@
/* Element Plus Message Customization */
.el-message {
border-radius: var(--border-radius-lg);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-large);
font-family: var(--font-family-primary);
font-weight: var(--font-weight-medium);
@@ -246,7 +268,7 @@
/* Element Plus Message Box Customization */
.el-message-box {
border-radius: var(--border-radius-lg);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-large);
}
@@ -256,7 +278,7 @@
}
.el-message-box__title {
font-family: var(--font-family-secondary);
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
}
@@ -304,37 +326,33 @@
/* Element Plus Tag Customization */
.el-tag {
border-radius: var(--border-radius-md);
border-radius: var(--border-radius-sm);
font-family: var(--font-family-primary);
font-weight: var(--font-weight-medium);
border: none;
}
.el-tag--primary {
background: rgba(var(--color-primary-rgb), 0.1);
color: var(--color-primary);
border-color: var(--color-primary);
background: var(--color-info-light);
color: var(--color-info);
}
.el-tag--success {
background: rgba(var(--color-success), 0.1);
background: var(--color-success-light);
color: var(--color-success);
border-color: var(--color-success);
}
.el-tag--warning {
background: rgba(var(--color-warning), 0.1);
background: var(--color-warning-light);
color: var(--color-warning);
border-color: var(--color-warning);
}
.el-tag--danger {
background: rgba(var(--color-danger), 0.1);
background: var(--color-danger-light);
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);
background: rgba(var(--color-primary-rgb), 0.1);
color: var(--color-primary);
}

View File

@@ -1,4 +1,4 @@
/* Global Styles */
/* Global Styles - 基于参考图设计风格 */
* {
margin: 0;
padding: 0;
@@ -17,7 +17,7 @@ body {
font-size: var(--font-size-base);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-background);
background: var(--color-background-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -25,12 +25,17 @@ body {
#app {
width: 100%;
height: 100%;
overflow-y: scroll;
}
/* Scrollbar Styling */
.el-container {
min-height: 100vh;
}
/* Scrollbar Styling - 更符合参考图的轻量级设计 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@@ -49,19 +54,36 @@ body {
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
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); }
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);
@@ -76,11 +98,12 @@ a {
}
a:hover {
color: var(--color-secondary);
color: var(--color-primary-dark);
}
/* Lists */
ul, ol {
ul,
ol {
margin-bottom: var(--spacing-md);
padding-left: var(--spacing-2xl);
}
@@ -121,7 +144,8 @@ table {
margin-bottom: var(--spacing-lg);
}
th, td {
th,
td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--color-border);
@@ -133,7 +157,7 @@ th {
}
tr:hover {
background: var(--color-border-light);
background: var(--color-background-tertiary);
}
/* Forms */
@@ -167,75 +191,197 @@ img {
}
/* Utilities */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.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); }
.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); }
.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); }
.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); }
.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); }
.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; }
.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; }
.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; }
.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; }
.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); }
.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; }
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
.position-relative {
position: relative;
}
.position-absolute {
position: absolute;
}
.position-fixed {
position: fixed;
}

View File

@@ -1,36 +1,62 @@
/* CSS Variables */
/* 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-primary: #00C897;
--color-primary-rgb: 0, 200, 151;
--color-primary-dark: #00A67C;
--color-success: #00b894;
--color-danger: #e74c3c;
--color-info: #14b8a6;
--color-warning: #f39c12;
/* 辅助色 */
--color-secondary: #00B4D8;
--color-secondary-rgb: 0, 180, 216;
--color-background: #f5f7fa;
--color-border: #e2e8f0;
--color-border-light: #f8fafc;
/* 功能色 */
--color-success: #2ECC71;
--color-success-rgb: 46, 204, 113;
--color-success-light: #E8F5E9;
--color-text-primary: #2d3748;
--color-text-secondary: #4a5568;
--color-text-light: #718096;
--color-text-white: #ffffff;
--color-danger: #E74C3C;
--color-danger-rgb: 231, 76, 60;
--color-danger-light: #FDE8E8;
--color-info: #1877F2;
--color-info-rgb: 24, 119, 242;
--color-info-light: #E3F2FD;
--color-warning: #FFA500;
--color-warning-rgb: 255, 165, 0;
--color-warning-light: #FFF3E0;
/* 背景色 */
--color-background: #FFFFFF;
--color-background-secondary: #FAFAFA;
--color-background-tertiary: #F8F9FA;
/* 深色导航栏 */
--color-sidebar-bg: #221E1E;
--color-header-bg: #221E1E;
/* 边框色 */
--color-border: #E9ECEF;
--color-border-light: #F0F0F0;
/* 文字颜色 */
--color-text-primary: #333333;
--color-text-secondary: #666666;
--color-text-light: #6C757D;
--color-text-white: #FFFFFF;
--color-text-disabled: #999999;
/* 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;
--spacing-xl: 20px;
--spacing-2xl: 24px;
--spacing-3xl: 32px;
--spacing-4xl: 48px;
/* Typography */
--font-family-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
@@ -60,10 +86,10 @@
--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);
/* Shadows - 轻量级阴影 */
--shadow-small: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.05);
--shadow-large: 0 8px 24px rgba(0, 0, 0, 0.12);
/* Transitions */
--transition-fast: 150ms;
@@ -76,7 +102,7 @@
--transition-box-shadow: box-shadow var(--transition-normal) ease;
/* Layout */
--header-height: 64px;
--header-height: 60px;
--sidebar-width: 240px;
--content-max-width: 1200px;
--content-max-width: 1400px;
}

View File

@@ -118,7 +118,7 @@
</el-dialog>
<!-- 显示设置对话框 -->
<el-dialog v-model="showDisplaySettingsDialog" title="大屏端显示设置" width="400px">
<el-dialog v-model="showDisplaySettingsDialog" title="大屏端显示设置" width="800px">
<el-form label-width="120px">
<el-form-item label="每行显示人数">
<el-input-number
@@ -131,6 +131,71 @@
设置大屏端名单每行显示的人数建议值2-5
</div>
</el-form-item>
<el-form-item label="字体颜色">
<el-color-picker v-model="tempDisplayFontColor" />
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
设置大屏端文字颜色轮次名称人名
</div>
</el-form-item>
<el-form-item label="奖品名称颜色">
<el-color-picker v-model="tempPrizeNameFontColor" />
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
设置奖品名称的文字颜色
</div>
</el-form-item>
<el-form-item label="轮次名称字号">
<el-input-number
v-model="tempRoundNameFontSize"
:min="16"
:max="120"
:step="1"
/>
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
设置轮次名称的字体大小建议值28-48
</div>
</el-form-item>
<el-form-item label="奖品名称字号">
<el-input-number
v-model="tempPrizeNameFontSize"
:min="16"
:max="120"
:step="1"
/>
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
设置奖品名称的字体大小建议值48-80
</div>
</el-form-item>
<el-form-item label="人名字号">
<el-input-number
v-model="tempParticipantFontSize"
:min="16"
:max="72"
:step="1"
/>
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
设置人名的字体大小建议值24-48
</div>
</el-form-item>
<el-form-item label="显示字段">
<el-checkbox-group v-model="tempDisplayFields">
<el-checkbox
v-for="field in store.fields"
:key="field.key"
:label="field.key"
>
{{ field.label }}
</el-checkbox>
</el-checkbox-group>
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
选择在大屏端显示的字段至少选择一个
</div>
</el-form-item>
<el-form-item label="显示字段标签">
<el-switch v-model="tempShowFieldLabels" />
<div style="margin-top: 8px; color: var(--color-text-light); font-size: 12px;">
开启后显示"姓名: 张三"格式关闭后只显示"张三"
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showDisplaySettingsDialog = false">取消</el-button>
@@ -169,15 +234,42 @@ const showShortcutGuide = ref(false)
// 显示设置
const showDisplaySettingsDialog = ref(false)
const tempColumnsPerRow = ref(3)
const tempDisplayFontColor = ref('#FFFFFF')
const tempPrizeNameFontColor = ref('#00B4D8')
const tempRoundNameFontSize = ref(36)
const tempPrizeNameFontSize = ref(64)
const tempParticipantFontSize = ref(32)
const tempDisplayFields = ref([])
const tempShowFieldLabels = ref(true)
const openDisplaySettings = () => {
tempColumnsPerRow.value = store.columnsPerRow
tempDisplayFontColor.value = store.displayFontColor
tempPrizeNameFontColor.value = store.prizeNameFontColor
tempRoundNameFontSize.value = store.roundNameFontSize
tempPrizeNameFontSize.value = store.prizeNameFontSize
tempParticipantFontSize.value = store.participantFontSize
tempDisplayFields.value = store.displayFields.length > 0 ? [...store.displayFields] : store.fields.map(f => f.key)
tempShowFieldLabels.value = store.showFieldLabels
showDisplaySettingsDialog.value = true
}
const saveDisplaySettings = async () => {
try {
// 验证至少选择一个字段
if (tempDisplayFields.value.length === 0) {
ElMessage.error('请至少选择一个显示字段')
return
}
await store.setColumnsPerRow(tempColumnsPerRow.value)
await store.setDisplayFontColor(tempDisplayFontColor.value)
await store.setPrizeNameFontColor(tempPrizeNameFontColor.value)
await store.setRoundNameFontSize(tempRoundNameFontSize.value)
await store.setPrizeNameFontSize(tempPrizeNameFontSize.value)
await store.setParticipantFontSize(tempParticipantFontSize.value)
await store.setDisplayFields(tempDisplayFields.value)
await store.setShowFieldLabels(tempShowFieldLabels.value)
showDisplaySettingsDialog.value = false
ElMessage.success('显示设置已保存')
} catch (error) {
@@ -242,8 +334,8 @@ const clearBackgroundImage = async () => {
}
.el-header {
background: var(--color-background);
border-bottom: 1px solid var(--color-border);
background: var(--color-header-bg);
border-bottom: 1px solid transparent;
display: flex;
align-items: center;
justify-content: space-between;
@@ -255,13 +347,7 @@ const clearBackgroundImage = async () => {
}
.el-header::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--color-secondary);
display: none;
}
.header-content {
@@ -276,30 +362,30 @@ const clearBackgroundImage = async () => {
font-family: var(--font-family-secondary);
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
color: var(--color-text-white);
}
.el-header .el-button {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-primary);
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--color-text-white);
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);
border-radius: var(--border-radius-sm);
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);
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
color: var(--color-text-white);
transform: none;
}
.el-aside {
background: var(--color-background);
border-right: 1px solid var(--color-border);
background: var(--color-sidebar-bg);
border-right: 1px solid transparent;
padding: var(--spacing-lg) 0;
}
@@ -311,14 +397,14 @@ const clearBackgroundImage = async () => {
.admin-menu .el-menu-item {
margin: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--border-radius-md);
color: var(--color-text-primary);
color: var(--color-text-white);
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);
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-white);
}
.admin-menu .el-menu-item.is-active {
@@ -331,7 +417,7 @@ const clearBackgroundImage = async () => {
}
.el-main {
padding: var(--spacing-3xl);
padding: var(--spacing-2xl);
max-width: 1400px;
margin: 0 auto;
width: 100%;

View File

@@ -1,53 +1,28 @@
<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="display-container" @keydown="handleKeydown" tabindex="0" :style="{ backgroundImage: store.backgroundImage ? `url(${store.backgroundImage})` : 'none' }">
<!-- 当前轮次信息 -->
<div class="current-round-info" v-if="selectedRound">
<div class="current-round-info" :class="{ centered: store.displayMode === 'scroll' && !store.isRolling }" 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 v-if="store.displayMode === 'scroll' && store.isRolling" class="scroll-mode">
<div class="scroll-list" :style="{ 'grid-template-columns': `repeat(${store.columnsPerRow}, 1fr)` }">
<div v-for="(participant, index) in getDisplayParticipants()" :key="participant.id || index" class="scroll-item">
<div class="scroll-name">{{ getParticipantName(participant) }}</div>
</div>
</div>
</div>
<!-- 抽奖结果模式 -->
<div v-else class="result-mode">
<div v-else-if="store.displayMode === 'result'" 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 v-for="(winner, index) in currentWinners" :key="winner.id || index" class="winner-item">
<div class="winner-name">{{ getParticipantName(winner.participant) }}</div>
</div>
</div>
</div>
<!-- 状态指示器 -->
<div class="status-indicator" :class="{ 'active': store.isRolling }">
{{ store.isRolling ? '抽奖中...' : '等待抽奖' }}
</div>
</div>
</template>
@@ -66,12 +41,12 @@ const displayList = computed(() => {
return store.participants
})
const getPrizeName = (prizeId) => {
const getPrizeName = prizeId => {
const prize = store.prizes.find(p => p.id === prizeId)
return prize ? prize.name : ''
}
const getParticipantName = (person) => {
const getParticipantName = person => {
if (!person) return '无数据'
if (typeof person === 'string') return person
@@ -83,16 +58,27 @@ const getParticipantName = (person) => {
// 收集所有配置字段的值
const displayParts = []
store.fields.forEach(field => {
// 只显示选中的字段
const fieldsToDisplay = store.displayFields.length > 0 ? store.displayFields : store.fields.map(f => f.key)
fieldsToDisplay.forEach(fieldKey => {
const field = store.fields.find(f => f.key === fieldKey)
if (field) {
const value = person[field.key]
if (value && value.trim()) {
// 根据设置决定是否显示字段标签
if (store.showFieldLabels) {
displayParts.push(`${field.label}: ${value}`)
} else {
displayParts.push(value)
}
}
}
})
// 如果有配置字段的值,返回所有字段
if (displayParts.length > 0) {
return displayParts.join(' | ')
return displayParts.join('\n')
}
// 如果配置的字段都没有值,尝试从参与者对象的所有字段中找
@@ -111,19 +97,25 @@ const getParticipantName = (person) => {
return '无数据'
}
const getParticipantDetails = (person) => {
// 已经在 getParticipantName 中显示所有字段,这里返回空
return ''
const getDisplayParticipants = () => {
const count = selectedRound.value ? selectedRound.value.count : store.columnsPerRow || 1
const startIndex = highlightIndex.value
const participants = []
for (let i = 0; i < count; i++) {
const index = (startIndex + i) % displayList.value.length
participants.push(displayList.value[index])
}
return participants
}
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))
return store.winners.filter(w => w.roundId === selectedRound.value.id).sort((a, b) => new Date(a.time) - new Date(b.time))
})
const handleKeydown = (e) => {
const handleKeydown = e => {
// 空格键:开始/停止抽奖
if (e.code === 'Space') {
e.preventDefault()
@@ -155,7 +147,7 @@ const handleKeydown = (e) => {
}
}
const selectRound = (round) => {
const selectRound = round => {
selectedRound.value = round
}
@@ -178,7 +170,8 @@ const startScroll = () => {
if (highlightInterval) clearInterval(highlightInterval)
highlightInterval = setInterval(() => {
highlightIndex.value = (highlightIndex.value + 1) % displayList.value.length
const step = selectedRound.value ? selectedRound.value.count : store.columnsPerRow || 1
highlightIndex.value = (highlightIndex.value + step) % displayList.value.length
}, 80)
}
@@ -246,83 +239,37 @@ onUnmounted(() => {
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);
margin-bottom: 40px;
}
.current-round-info.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-bottom: 0;
z-index: 10;
}
.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);
font-size: v-bind('store.roundNameFontSize + "px"');
font-weight: bold;
color: v-bind('store.displayFontColor');
margin-bottom: 15px;
}
.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);
font-size: v-bind('store.prizeNameFontSize + "px"');
font-weight: bold;
color: v-bind('store.prizeNameFontColor');
}
/* 滚动模式 */
@@ -334,33 +281,34 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0 var(--spacing-4xl);
padding: 0 40px;
}
.scroll-list {
display: grid;
gap: 30px;
width: 100%;
max-width: 1600px;
}
.scroll-item {
font-size: 64px;
font-weight: var(--font-weight-bold);
color: var(--color-text-white);
padding: var(--spacing-2xl) var(--spacing-4xl);
font-size: v-bind('store.participantFontSize + "px"');
font-weight: bold;
color: v-bind('store.displayFontColor');
padding: 20px 30px;
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;
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.scroll-item.highlight {
transform: scale(1.05);
background: rgba(var(--color-secondary-rgb), 0.4);
border-color: rgba(var(--color-secondary-rgb), 0.8);
.scroll-name {
white-space: pre-line;
word-wrap: break-word;
word-break: break-all;
width: 100%;
}
/* 结果模式 */
@@ -371,84 +319,34 @@ onUnmounted(() => {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 var(--spacing-4xl);
padding: 0 40px;
}
.winners-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-2xl);
gap: 30px;
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);
font-size: v-bind('store.participantFontSize + "px"');
font-weight: bold;
color: v-bind('store.displayFontColor');
padding: 20px 30px;
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);
border-radius: 12px;
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 {
white-space: pre-line;
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>

View File

@@ -1,43 +1,139 @@
<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 class="participants-container">
<!-- 页面标题和操作按钮 -->
<div class="page-header">
<h2 class="page-title">名单管理</h2>
<div class="action-buttons">
<el-button type="info" size="default" @click="showFieldConfig">
<el-icon><Setting /></el-icon>
字段配置
</el-button>
<el-button type="primary" size="default" @click="showImportDialog = true">
<el-icon><Upload /></el-icon>
导入
</el-button>
<el-button type="success" size="default" @click="exportParticipants">
<el-icon><Download /></el-icon>
导出
</el-button>
<el-button type="danger" size="default" @click="clearParticipants">
<el-icon><Delete /></el-icon>
清空
</el-button>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-card">
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-primary-light)">
<el-icon><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ store.participants.length }}</div>
<div class="stat-label">总人数</div>
</div>
</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-card class="form-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><Plus /></el-icon>
添加参与者
</span>
</div>
</template>
<el-form :model="newParticipantData" label-width="150px" size="default">
<el-row :gutter="20">
<el-col :span="10" v-for="field in store.fields" :key="field.id">
<el-form-item :label="field.label" :required="field.required">
<el-input
v-model="newParticipantData[field.key]"
:placeholder="`请输入${field.label}`"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="addParticipant" style="width: 100%"> 添加 </el-button>
<el-button type="primary" @click="addParticipant" size="default">
<el-icon><Check /></el-icon>
添加
</el-button>
<el-button @click="initNewParticipantData" size="default">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<!-- 参与者表 -->
<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">
<!-- 参与者 -->
<el-card class="list-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><List /></el-icon>
参与者列表
</span>
<span class="card-subtitle"> {{ store.participants.length }} </span>
</div>
</template>
<el-table
:data="paginatedParticipants"
style="width: 100%"
stripe
max-height="500"
:empty-text="'暂无参与者,请先添加或导入'"
>
<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="180" 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>
<el-button
type="info"
size="small"
link
@click="showParticipantDetail(row)"
v-if="store.fields.length > 1"
>
<el-icon><View /></el-icon>
详情
</el-button>
<el-button
type="danger"
size="small"
link
@click="removeParticipant(row.id)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="participant-count"> {{ store.participants.length }} </div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="store.participants.length"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 字段配置对话框 -->
@@ -45,13 +141,35 @@
<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-input
v-model="field.key"
placeholder="字段键"
size="default"
style="width: 140px"
/>
<el-input
v-model="field.label"
placeholder="字段名称"
size="default"
style="width: 140px"
/>
<el-checkbox v-model="field.required">必填</el-checkbox>
<el-button type="danger" size="small" @click="removeField(field.id)" :disabled="tempFields.length <= 1">删除</el-button>
<el-button
type="danger"
size="small"
link
@click="removeField(field.id)"
:disabled="tempFields.length <= 1"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-button type="primary" @click="addField" style="margin-top: 15px">添加字段</el-button>
<el-button type="primary" @click="addField" style="margin-top: 15px; width: 100%">
<el-icon><Plus /></el-icon>
添加字段
</el-button>
</div>
<template #footer>
<el-button @click="showFieldDialog = false">取消</el-button>
@@ -61,11 +179,22 @@
<!-- 导入对话框 -->
<el-dialog v-model="showImportDialog" title="导入名单" width="500px">
<el-upload ref="uploadRef" :auto-upload="false" :on-change="handleFileChange" :limit="1" accept=".csv" drag>
<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>
<div class="el-upload__text">
拖拽文件到此处或 <em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip"> 支持 .csv 格式文件第一行为字段名后续行为数据 </div>
<div class="el-upload__tip">
支持 .csv 格式文件第一行为字段名后续行为数据
</div>
</template>
</el-upload>
<template #footer>
@@ -73,12 +202,25 @@
<el-button type="primary" @click="handleImport" :disabled="!selectedFile">导入</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { useLotteryStore } from '../../store'
import { UploadFilled } from '@element-plus/icons-vue'
import {
UploadFilled,
Setting,
Upload,
Download,
Delete,
User,
Plus,
Check,
RefreshLeft,
List,
View
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const store = useLotteryStore()
@@ -88,6 +230,25 @@ onMounted(async () => {
await store.initialize()
})
// 分页相关
const currentPage = ref(1)
const pageSize = ref(20)
const paginatedParticipants = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return store.participants.slice(start, end)
})
const handlePageChange = (page) => {
currentPage.value = page
}
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
}
// 字段配置
const showFieldDialog = ref(false)
const tempFields = ref([])
@@ -253,112 +414,202 @@ const clearParticipants = () => {
</script>
<style scoped>
.card {
border-radius: var(--border-radius-lg);
.participants-container {
padding: var(--spacing-2xl);
max-width: 1400px;
margin: 0 auto;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--color-border);
}
.page-title {
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);
}
.action-buttons {
display: flex;
gap: var(--spacing-md);
}
.action-buttons .el-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* 统计卡片 */
.stats-card {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) var(--spacing-2xl);
background: var(--color-background);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: 1px solid var(--color-border);
flex: 1;
max-width: 300px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: var(--color-text-white);
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
line-height: 1;
margin-bottom: var(--spacing-xs);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
/* 卡片通用样式 */
.form-card,
.list-card {
margin-bottom: var(--spacing-2xl);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: none;
background: var(--color-background);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.single-add-form {
/* 表单样式 */
.form-card :deep(.el-form-item) {
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) {
.form-card :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);
.form-card :deep(.el-button) {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.participant-table {
margin-bottom: var(--spacing-md);
}
.participant-table :deep(.el-table) {
/* 表格样式 */
.list-card :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);
.list-card :deep(.el-table th) {
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-weight: var(--font-weight-semibold);
font-family: var(--font-family-secondary);
font-family: var(--font-family-primary);
font-size: var(--font-size-base);
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-lg);
}
.participant-table :deep(.el-table td) {
.list-card :deep(.el-table td) {
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-size-base);
padding: var(--spacing-lg);
}
.participant-table :deep(.el-table .el-table__row:hover > td) {
background: rgba(var(--color-primary-rgb), 0.05);
.list-card :deep(.el-table .el-table__row:hover > td) {
background: var(--color-background-tertiary);
}
.participant-table :deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: rgba(var(--color-primary-rgb), 0.03);
.list-card :deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: var(--color-background);
}
.participant-table :deep(.el-table__body-wrapper) {
max-height: 400px;
.list-card :deep(.el-table__body-wrapper) {
max-height: 500px;
overflow-y: auto;
}
.participant-table :deep(.el-table__body-wrapper::-webkit-scrollbar) {
.list-card :deep(.el-table__body-wrapper::-webkit-scrollbar) {
width: 6px;
}
.participant-table :deep(.el-table__body-wrapper::-webkit-scrollbar-track) {
.list-card :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) {
.list-card :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) {
.list-card :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
background: var(--color-text-secondary);
}
.participant-count {
text-align: right;
color: var(--color-text-light);
/* 分页容器 */
.pagination-container {
display: flex;
justify-content: center;
padding: var(--spacing-xl) 0;
margin-top: var(--spacing-lg);
}
.card-subtitle {
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);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
/* 字段配置样式 */
.field-config {
padding: var(--spacing-md);
}
@@ -392,16 +643,15 @@ const clearParticipants = () => {
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
padding: var(--spacing-md);
border: 1px solid var(--color-border);
padding: var(--spacing-md) var(--spacing-lg);
border: 1px solid var(--color-border-light);
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);
border-color: var(--color-primary);
box-shadow: var(--shadow-small);
transform: none;
}
</style>

View File

@@ -1,44 +1,102 @@
<template>
<el-card class="card">
<template #header>
<div class="card-header">
<span>奖品管理</span>
<el-button type="primary" size="small" @click="showPrizeDialog = true">
<div class="prizes-container">
<!-- 页面标题和操作按钮 -->
<div class="page-header">
<h2 class="page-title">奖品管理</h2>
<div class="action-buttons">
<el-button type="primary" size="default" @click="showPrizeDialog = true">
<el-icon><Plus /></el-icon>
添加奖品
</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)"
>
<!-- 统计信息 -->
<div class="stats-card">
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-warning-light)">
<el-icon><Present /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ store.prizes.length }}</div>
<div class="stat-label">奖品种类</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-info-light)">
<el-icon><Box /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ totalStock }}</div>
<div class="stat-label">总库存</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-success-light)">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ totalUsed }}</div>
<div class="stat-label">已发放</div>
</div>
</div>
</div>
<!-- 奖品列表 -->
<el-card class="list-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><List /></el-icon>
奖品列表
</span>
</div>
</template>
<div class="prize-list">
<div v-for="prize in store.prizes" :key="prize.id" class="prize-item">
<div class="prize-icon">
<el-icon><Present /></el-icon>
</div>
<div class="prize-content">
<div class="prize-name">{{ prize.name }}</div>
<div class="prize-stock">
<span class="stock-label">库存:</span>
<span class="stock-value">{{ prize.used }}/{{ prize.stock }}</span>
<el-progress
:percentage="stockPercentage(prize)"
:status="getStockStatus(prize)"
:stroke-width="8"
/>
</div>
</div>
<div class="prize-actions">
<el-button type="danger" size="small" link @click="removePrize(prize.id)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-empty v-if="store.prizes.length === 0" description="暂无奖品,请先添加" />
</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-dialog v-model="showPrizeDialog" title="添加奖品" width="500px">
<el-form :model="newPrize" label-width="100px" size="default">
<el-form-item label="奖品名称" required>
<el-input
v-model="newPrize.name"
placeholder="例如:一等奖"
clearable
/>
</el-form-item>
<el-form-item label="库存数量">
<el-input-number v-model="newPrize.stock" :min="1" />
<el-form-item label="库存数量" required>
<el-input-number
v-model="newPrize.stock"
:min="1"
:max="9999"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -46,12 +104,20 @@
<el-button type="primary" @click="addPrize">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useLotteryStore } from '../../store'
import { Delete } from '@element-plus/icons-vue'
import {
Delete,
Plus,
Present,
Box,
CircleCheck,
List
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const store = useLotteryStore()
@@ -61,6 +127,15 @@ onMounted(async () => {
await store.initialize()
})
// 计算属性
const totalStock = computed(() => {
return store.prizes.reduce((sum, prize) => sum + prize.stock, 0)
})
const totalUsed = computed(() => {
return store.prizes.reduce((sum, prize) => sum + prize.used, 0)
})
// 奖品管理
const showPrizeDialog = ref(false)
const newPrize = ref({
@@ -68,14 +143,36 @@ const newPrize = ref({
stock: 1
})
// 计算库存百分比
const stockPercentage = (prize) => {
if (prize.stock === 0) return 0
return Math.round((prize.used / prize.stock) * 100)
}
// 获取库存状态
const getStockStatus = (prize) => {
const percentage = stockPercentage(prize)
if (percentage >= 100) return 'exception'
if (percentage >= 80) return 'warning'
return 'success'
}
const addPrize = () => {
if (newPrize.value.name.trim()) {
if (!newPrize.value.name.trim()) {
ElMessage.error('请输入奖品名称')
return
}
if (newPrize.value.stock < 1) {
ElMessage.error('库存数量必须大于0')
return
}
store.addPrize(newPrize.value)
newPrize.value = { name: '', stock: 1 }
showPrizeDialog.value = false
ElMessage.success('添加成功')
}
}
const removePrize = (id) => {
store.removePrize(id)
@@ -84,80 +181,190 @@ const removePrize = (id) => {
</script>
<style scoped>
.card {
border-radius: var(--border-radius-lg);
.prizes-container {
padding: var(--spacing-2xl);
max-width: 1400px;
margin: 0 auto;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--color-border);
}
.page-title {
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);
}
.action-buttons {
display: flex;
gap: var(--spacing-md);
}
.action-buttons .el-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* 统计卡片 */
.stats-card {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) var(--spacing-2xl);
background: var(--color-background);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: 1px solid var(--color-border);
flex: 1;
max-width: 300px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: var(--color-text-white);
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
line-height: 1;
margin-bottom: var(--spacing-xs);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
/* 列表卡片 */
.list-card {
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: none;
background: var(--color-background);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* 奖品列表 */
.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);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.prize-item {
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
border: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl);
border: 1px solid var(--color-border-light);
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);
border-color: var(--color-primary);
box-shadow: var(--shadow-small);
transform: none;
}
.prize-info {
.prize-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-md);
background: var(--color-warning-light);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: var(--color-warning);
flex-shrink: 0;
}
.prize-content {
flex: 1;
min-width: 0;
}
.prize-name {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-xs);
margin-bottom: var(--spacing-md);
font-family: var(--font-family-primary);
color: var(--color-text-primary);
}
.prize-stock {
color: var(--color-text-light);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.stock-label {
font-size: var(--font-size-sm);
font-family: var(--font-family-primary);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
.stock-value {
font-size: var(--font-size-base);
color: var(--color-text-primary);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-xs);
}
.prize-actions {
display: flex;
gap: var(--spacing-md);
flex-shrink: 0;
}
.prize-actions .el-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
</style>

View File

@@ -1,14 +1,57 @@
<template>
<el-card class="card">
<template #header>
<div class="card-header">
<span>轮次管理</span>
<el-button type="primary" size="small" @click="showRoundDialog = true">
<div class="rounds-container">
<!-- 页面标题和操作按钮 -->
<div class="page-header">
<h2 class="page-title">轮次管理</h2>
<div class="action-buttons">
<el-button type="primary" size="default" @click="showRoundDialog = true">
<el-icon><Plus /></el-icon>
添加轮次
</el-button>
</div>
</template>
</div>
<!-- 统计信息 -->
<div class="stats-card">
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-info-light)">
<el-icon><List /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ store.rounds.length }}</div>
<div class="stat-label">总轮次</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-success-light)">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ completedRounds }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-warning-light)">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ pendingRounds }}</div>
<div class="stat-label">待进行</div>
</div>
</div>
</div>
<!-- 轮次列表 -->
<el-card class="list-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><List /></el-icon>
轮次列表
</span>
</div>
</template>
<div class="round-list">
<div
v-for="round in store.rounds"
@@ -16,42 +59,76 @@
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 class="round-index">
<span class="index-number">{{ getRoundIndex(round) }}</span>
</div>
<div class="round-content">
<div class="round-name">{{ round.name }}</div>
<div class="round-detail">
<span class="detail-label">奖品:</span>
<span class="detail-value">{{ getPrizeName(round.prizeId) }}</span>
</div>
<div class="round-detail">
<span class="detail-label">抽取人数:</span>
<span class="detail-value">{{ round.count }}</span>
</div>
</div>
<div class="round-status">
<el-tag
:type="round.completed ? 'success' : 'info'"
size="small"
>
{{ round.completed ? '已完成' : '待进行' }}
</el-tag>
</div>
<div class="round-actions">
<el-button
type="danger"
size="small"
circle
link
@click="removeRound(round.id)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-empty v-if="store.rounds.length === 0" description="暂无轮次,请先添加" />
</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-dialog v-model="showRoundDialog" title="添加轮次" width="500px">
<el-form :model="newRound" label-width="100px" size="default">
<el-form-item label="轮次名称" required>
<el-input
v-model="newRound.name"
placeholder="例如:第一轮"
clearable
/>
</el-form-item>
<el-form-item label="选择奖品">
<el-select v-model="newRound.prizeId" placeholder="请选择奖品" style="width: 100%">
<el-form-item label="选择奖品" required>
<el-select
v-model="newRound.prizeId"
placeholder="请选择奖品"
style="width: 100%"
clearable
>
<el-option
v-for="prize in store.prizes"
v-for="prize in availablePrizes"
: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 label="抽取人数" required>
<el-input-number
v-model="newRound.count"
:min="1"
:max="9999"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -59,12 +136,19 @@
<el-button type="primary" @click="addRound">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useLotteryStore } from '../../store'
import { Delete } from '@element-plus/icons-vue'
import {
Delete,
Plus,
List,
CircleCheck,
Clock
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const store = useLotteryStore()
@@ -74,6 +158,32 @@ onMounted(async () => {
await store.initialize()
})
// 计算属性
const completedRounds = computed(() => {
return store.rounds.filter(r => r.completed).length
})
const pendingRounds = computed(() => {
return store.rounds.filter(r => !r.completed).length
})
const availablePrizes = computed(() => {
return store.prizes.filter(prize => {
const available = prize.stock - prize.used
return available > 0
})
})
// 获取轮次索引
const getRoundIndex = (round) => {
return store.rounds.findIndex(r => r.id === round.id) + 1
}
const getPrizeName = (prizeId) => {
const prize = store.prizes.find(p => p.id === prizeId)
return prize ? prize.name : ''
}
// 轮次管理
const showRoundDialog = ref(false)
const newRound = ref({
@@ -82,20 +192,26 @@ const newRound = ref({
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()) {
ElMessage.error('请输入轮次名称')
return
}
if (!newRound.value.prizeId) {
ElMessage.error('请选择奖品')
return
}
if (newRound.value.count < 1) {
ElMessage.error('抽取人数必须大于0')
return
}
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) => {
@@ -105,61 +221,134 @@ const removeRound = (id) => {
</script>
<style scoped>
.card {
border-radius: var(--border-radius-lg);
.rounds-container {
padding: var(--spacing-2xl);
max-width: 1400px;
margin: 0 auto;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--color-border);
}
.page-title {
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);
}
.action-buttons {
display: flex;
gap: var(--spacing-md);
}
.action-buttons .el-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* 统计卡片 */
.stats-card {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) var(--spacing-2xl);
background: var(--color-background);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: 1px solid var(--color-border);
flex: 1;
max-width: 300px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: var(--color-text-white);
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
line-height: 1;
margin-bottom: var(--spacing-xs);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
/* 列表卡片 */
.list-card {
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: none;
background: var(--color-background);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* 轮次列表 */
.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);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.round-item {
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
border: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl);
border: 1px solid var(--color-border-light);
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);
border-color: var(--color-primary);
box-shadow: var(--shadow-small);
transform: none;
}
.round-item.completed {
@@ -167,33 +356,68 @@ const removeRound = (id) => {
background: var(--color-border-light);
}
.round-info {
.round-index {
flex-shrink: 0;
}
.index-number {
width: 40px;
height: 40px;
border-radius: var(--border-radius-md);
background: var(--color-primary-light);
color: var(--color-primary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-lg);
}
.round-content {
flex: 1;
min-width: 0;
}
.round-name {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-xs);
margin-bottom: var(--spacing-md);
font-family: var(--font-family-primary);
color: var(--color-text-primary);
}
.round-detail {
color: var(--color-text-light);
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.detail-label {
font-size: var(--font-size-sm);
font-family: var(--font-family-primary);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
.detail-value {
font-size: var(--font-size-base);
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
}
.round-status {
flex-shrink: 0;
}
.round-actions {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-md);
flex-shrink: 0;
}
.round-actions .el-button {
flex: 1;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
</style>

View File

@@ -1,32 +1,123 @@
<template>
<el-card class="card">
<template #header>
<div class="card-header">
<span>中奖记录</span>
<div>
<el-button type="success" size="small" @click="exportWinners">
<div class="winners-container">
<!-- 页面标题和操作按钮 -->
<div class="page-header">
<h2 class="page-title">中奖记录</h2>
<div class="action-buttons">
<el-button type="success" size="default" @click="exportWinners">
<el-icon><Download /></el-icon>
导出
</el-button>
<el-button type="danger" size="small" @click="resetLottery">
<el-button type="danger" size="default" @click="resetLottery">
<el-icon><Delete /></el-icon>
清空
</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="时间" />
<!-- 统计信息 -->
<div class="stats-card">
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-success-light)">
<el-icon><Trophy /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ store.winners.length }}</div>
<div class="stat-label">中奖人数</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-info-light)">
<el-icon><Present /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ uniquePrizes }}</div>
<div class="stat-label">奖品种类</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon" style="background: var(--color-warning-light)">
<el-icon><List /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ uniqueRounds }}</div>
<div class="stat-label">涉及轮次</div>
</div>
</div>
</div>
<!-- 中奖记录列表 -->
<el-card class="list-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon><List /></el-icon>
中奖记录
</span>
<span class="card-subtitle"> {{ store.winners.length }} </span>
</div>
</template>
<el-table
:data="paginatedWinners"
style="width: 100%"
stripe
max-height="500"
:empty-text="'暂无中奖记录'"
>
<!-- 动态生成字段列 -->
<el-table-column
v-for="field in store.fields"
:key="field.key"
:prop="field.key"
:label="field.label"
min-width="120"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.participant ? row.participant[field.key] : '' }}
</template>
</el-table-column>
<el-table-column prop="prizeName" label="奖品" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="success" size="small">{{ row.prizeName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="roundName" label="轮次" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="info" size="small">{{ row.roundName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="time" label="中奖时间" min-width="180" show-overflow-tooltip />
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="store.winners.length"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useLotteryStore } from '../../store'
import { ElMessage } from 'element-plus'
import {
Download,
Delete,
Trophy,
Present,
List
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const store = useLotteryStore()
@@ -35,10 +126,59 @@ onMounted(async () => {
await store.initialize()
})
// 分页相关
const currentPage = ref(1)
const pageSize = ref(20)
const paginatedWinners = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return store.winners.slice(start, end)
})
const handlePageChange = (page) => {
currentPage.value = page
}
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
}
// 计算属性
const uniquePrizes = computed(() => {
const prizes = new Set(store.winners.map(w => w.prizeName))
return prizes.size
})
const uniqueRounds = computed(() => {
const rounds = new Set(store.winners.map(w => w.roundName))
return rounds.size
})
// 重置抽奖
const resetLottery = () => {
if (store.winners.length === 0) {
ElMessage.warning('暂无中奖记录')
return
}
ElMessageBox.confirm(
'确定要清空所有中奖记录吗?此操作不可恢复。',
'确认清空',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
store.resetLottery()
ElMessage.success('已重置')
ElMessage.success('已清空中奖记录')
})
.catch(() => {
// 用户取消操作
})
}
// 导出中奖名单
@@ -53,47 +193,179 @@ const exportWinners = () => {
</script>
<style scoped>
.card {
border-radius: var(--border-radius-lg);
.winners-container {
padding: var(--spacing-2xl);
max-width: 1400px;
margin: 0 auto;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--color-border);
}
.page-title {
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);
}
.action-buttons {
display: flex;
gap: var(--spacing-md);
}
.action-buttons .el-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* 统计卡片 */
.stats-card {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) var(--spacing-2xl);
background: var(--color-background);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: 1px solid var(--color-border);
flex: 1;
max-width: 300px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: var(--color-text-white);
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
line-height: 1;
margin-bottom: var(--spacing-xs);
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
/* 列表卡片 */
.list-card {
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-small);
border: none;
background: var(--color-background);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-semibold);
font-size: var(--font-size-lg);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
:deep(.el-table) {
.card-subtitle {
font-size: var(--font-size-sm);
color: var(--color-text-light);
font-weight: var(--font-weight-medium);
}
/* 表格样式 */
.list-card :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);
.list-card :deep(.el-table th) {
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-weight: var(--font-weight-semibold);
font-family: var(--font-family-secondary);
font-family: var(--font-family-primary);
font-size: var(--font-size-base);
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-lg);
}
:deep(.el-table td) {
.list-card :deep(.el-table td) {
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-size-base);
padding: var(--spacing-lg);
}
:deep(.el-table .el-table__row:hover > td) {
background: rgba(var(--color-primary-rgb), 0.05);
.list-card :deep(.el-table .el-table__row:hover > td) {
background: var(--color-background-tertiary);
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: rgba(var(--color-primary-rgb), 0.03);
.list-card :deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: var(--color-background);
}
.list-card :deep(.el-table__body-wrapper) {
max-height: 500px;
overflow-y: auto;
}
.list-card :deep(.el-table__body-wrapper::-webkit-scrollbar) {
width: 6px;
}
.list-card :deep(.el-table__body-wrapper::-webkit-scrollbar-track) {
background: var(--color-border);
border-radius: var(--border-radius-sm);
}
.list-card :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb) {
background: var(--color-text-light);
border-radius: var(--border-radius-sm);
}
.list-card :deep(.el-table__body-wrapper::-webkit-scrollbar-thumb:hover) {
background: var(--color-text-secondary);
}
/* 分页容器 */
.pagination-container {
display: flex;
justify-content: center;
padding: var(--spacing-xl) 0;
margin-top: var(--spacing-lg);
}
</style>