You've already forked rollingDraw
初始化提交
This commit is contained in:
24
display/.gitignore
vendored
Normal file
24
display/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
13
display/index.html
Normal file
13
display/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>display</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1739
display/package-lock.json
generated
Normal file
1739
display/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
display/package.json
Normal file
20
display/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "display",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-plus": "^2.13.1",
|
||||
"pinia": "^3.0.4"
|
||||
}
|
||||
}
|
||||
1
display/public/vite.svg
Normal file
1
display/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
451
display/src/App.vue
Normal file
451
display/src/App.vue
Normal file
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<div class="display-container" @keydown="handleKeydown" tabindex="0">
|
||||
<!-- 快捷键提示 -->
|
||||
<div class="shortcut-hints" v-if="!store.isRolling">
|
||||
<div class="hint-item">
|
||||
<span class="key">← →</span>
|
||||
<span class="desc">切换轮次</span>
|
||||
</div>
|
||||
<div class="hint-item">
|
||||
<span class="key">Space</span>
|
||||
<span class="desc">开始/停止</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前轮次信息 -->
|
||||
<div class="current-round-info" v-if="selectedRound">
|
||||
<div class="round-name">{{ selectedRound.name }}</div>
|
||||
<div class="prize-name">{{ getPrizeName(selectedRound.prizeId) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动模式 -->
|
||||
<div v-if="store.displayMode === 'scroll'" class="scroll-mode">
|
||||
<div class="scroll-list" :style="{ transform: `translateY(${scrollOffset}px)` }">
|
||||
<div
|
||||
v-for="(person, index) in displayList"
|
||||
:key="person.id || index"
|
||||
class="scroll-item"
|
||||
:class="{ 'highlight': index === highlightIndex }"
|
||||
>
|
||||
{{ getParticipantName(person) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 抽奖结果模式 -->
|
||||
<div v-else class="result-mode">
|
||||
<div class="winners-list">
|
||||
<div
|
||||
v-for="(winner, index) in currentWinners"
|
||||
:key="winner.id || index"
|
||||
class="winner-item"
|
||||
:style="{ animationDelay: `${index * 0.2}s` }"
|
||||
>
|
||||
<div class="winner-name">{{ getParticipantName(winner) }}</div>
|
||||
<div class="winner-info" v-if="store.fields.length > 1">
|
||||
{{ getParticipantDetails(winner) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态指示器 -->
|
||||
<div class="status-indicator" :class="{ 'active': store.isRolling }">
|
||||
{{ store.isRolling ? '抽奖中...' : '等待抽奖' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useLotteryStore } from './store'
|
||||
|
||||
const store = useLotteryStore()
|
||||
|
||||
const selectedRound = ref(null)
|
||||
const scrollOffset = ref(0)
|
||||
const highlightIndex = ref(0)
|
||||
let scrollInterval = null
|
||||
let highlightInterval = null
|
||||
|
||||
const displayList = computed(() => {
|
||||
if (store.participants.length === 0) return ['暂无名单']
|
||||
return store.participants
|
||||
})
|
||||
|
||||
const getPrizeName = (prizeId) => {
|
||||
const prize = store.prizes.find(p => p.id === prizeId)
|
||||
return prize ? prize.name : ''
|
||||
}
|
||||
|
||||
const getParticipantName = (person) => {
|
||||
if (typeof person === 'string') return person
|
||||
|
||||
// 首先尝试从配置的字段中查找 name 字段
|
||||
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 getParticipantDetails = (person) => {
|
||||
const details = []
|
||||
|
||||
// 首先显示配置的字段
|
||||
store.fields.forEach(field => {
|
||||
if (field.key !== 'name' && person[field.key]) {
|
||||
details.push(`${field.label}: ${person[field.key]}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果配置的字段没有显示任何内容,尝试显示所有字段
|
||||
if (details.length === 0) {
|
||||
const allKeys = Object.keys(person).filter(key => key !== 'id')
|
||||
allKeys.forEach(key => {
|
||||
if (key.toLowerCase() !== 'name' && person[key]) {
|
||||
details.push(`${key}: ${person[key]}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return details.join(' | ')
|
||||
}
|
||||
|
||||
const currentWinners = computed(() => {
|
||||
if (!store.currentRound) return []
|
||||
return store.winners
|
||||
.filter(w => w.roundId === store.currentRound.id)
|
||||
.sort((a, b) => new Date(a.time) - new Date(b.time))
|
||||
})
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
// 空格键:开始/停止抽奖
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault()
|
||||
if (store.isRolling) {
|
||||
handleStopLottery()
|
||||
} else if (selectedRound.value && !selectedRound.value.completed) {
|
||||
handleStartLottery()
|
||||
}
|
||||
}
|
||||
// 左右方向键:切换轮次
|
||||
else if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') {
|
||||
e.preventDefault()
|
||||
if (store.rounds.length === 0) return
|
||||
|
||||
let currentIndex = -1
|
||||
if (selectedRound.value) {
|
||||
currentIndex = store.rounds.findIndex(r => r.id === selectedRound.value.id)
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowLeft') {
|
||||
// 向左切换
|
||||
currentIndex = currentIndex <= 0 ? store.rounds.length - 1 : currentIndex - 1
|
||||
} else {
|
||||
// 向右切换
|
||||
currentIndex = currentIndex >= store.rounds.length - 1 ? 0 : currentIndex + 1
|
||||
}
|
||||
|
||||
selectRound(store.rounds[currentIndex])
|
||||
}
|
||||
}
|
||||
|
||||
const selectRound = (round) => {
|
||||
selectedRound.value = round
|
||||
}
|
||||
|
||||
const handleStartLottery = () => {
|
||||
if (!selectedRound.value) return
|
||||
try {
|
||||
store.startLottery(selectedRound.value)
|
||||
startScroll()
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopLottery = () => {
|
||||
store.stopLottery()
|
||||
stopScroll()
|
||||
}
|
||||
|
||||
const startScroll = () => {
|
||||
if (scrollInterval) clearInterval(scrollInterval)
|
||||
if (highlightInterval) clearInterval(highlightInterval)
|
||||
|
||||
scrollInterval = setInterval(() => {
|
||||
scrollOffset.value -= 2
|
||||
if (Math.abs(scrollOffset.value) >= 60) {
|
||||
scrollOffset.value = 0
|
||||
}
|
||||
}, 50)
|
||||
|
||||
highlightInterval = setInterval(() => {
|
||||
highlightIndex.value = (highlightIndex.value + 1) % displayList.value.length
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const stopScroll = () => {
|
||||
if (scrollInterval) {
|
||||
clearInterval(scrollInterval)
|
||||
scrollInterval = null
|
||||
}
|
||||
if (highlightInterval) {
|
||||
clearInterval(highlightInterval)
|
||||
highlightInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 自动聚焦以便接收键盘事件
|
||||
document.querySelector('.display-container')?.focus()
|
||||
|
||||
if (store.displayMode === 'scroll' && store.isRolling) {
|
||||
startScroll()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopScroll()
|
||||
store.stopSync()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.display-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
outline: none;
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.display-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: v-bind('store.backgroundImage ? `url(${store.backgroundImage})` : "none"');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.display-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.display-container > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 快捷键提示 */
|
||||
.shortcut-hints {
|
||||
position: fixed;
|
||||
top: var(--spacing-xl);
|
||||
left: var(--spacing-xl);
|
||||
display: flex;
|
||||
gap: var(--spacing-lg);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.hint-item {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.key {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text-white);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-sm);
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(var(--color-secondary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--color-text-white);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 当前轮次信息 */
|
||||
.current-round-info {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-4xl);
|
||||
}
|
||||
|
||||
.round-name {
|
||||
font-size: var(--font-size-4xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-white);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
||||
font-family: var(--font-family-secondary);
|
||||
}
|
||||
|
||||
.prize-name {
|
||||
font-size: 64px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-secondary);
|
||||
text-shadow: 0 0 30px rgba(var(--color-secondary-rgb), 0.8);
|
||||
font-family: var(--font-family-secondary);
|
||||
}
|
||||
|
||||
/* 滚动模式 */
|
||||
.scroll-mode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
.scroll-item {
|
||||
font-size: var(--font-size-4xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
padding: var(--spacing-md) var(--spacing-3xl);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
transition: var(--transition-normal);
|
||||
font-family: var(--font-family-secondary);
|
||||
}
|
||||
|
||||
.scroll-item.highlight {
|
||||
color: var(--color-text-white);
|
||||
font-size: 72px;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.8);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* 结果模式 */
|
||||
.result-mode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.winners-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
justify-content: center;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.winner-item {
|
||||
font-size: 56px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text-white);
|
||||
padding: var(--spacing-xl) var(--spacing-4xl);
|
||||
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);
|
||||
}
|
||||
|
||||
@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>
|
||||
9
display/src/counter.ts
Normal file
9
display/src/counter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function setupCounter(element: HTMLButtonElement) {
|
||||
let counter = 0
|
||||
const setCounter = (count: number) => {
|
||||
counter = count
|
||||
element.innerHTML = `count is ${counter}`
|
||||
}
|
||||
element.addEventListener('click', () => setCounter(counter + 1))
|
||||
setCounter(0)
|
||||
}
|
||||
9
display/src/main.js
Normal file
9
display/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import './styles/variables.css'
|
||||
import './style.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
24
display/src/main.ts
Normal file
24
display/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import './style.css'
|
||||
import typescriptLogo from './typescript.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import { setupCounter } from './counter.ts'
|
||||
|
||||
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="${viteLogo}" class="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://www.typescriptlang.org/" target="_blank">
|
||||
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
|
||||
</a>
|
||||
<h1>Vite + TypeScript</h1>
|
||||
<div class="card">
|
||||
<button id="counter" type="button"></button>
|
||||
</div>
|
||||
<p class="read-the-docs">
|
||||
Click on the Vite and TypeScript logos to learn more
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
|
||||
153
display/src/store/index.js
Normal file
153
display/src/store/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { indexedDB } from '@utils/indexedDB'
|
||||
|
||||
export const useLotteryStore = defineStore('lottery', () => {
|
||||
// 初始化数据
|
||||
const fields = ref([])
|
||||
const participants = ref([])
|
||||
const prizes = ref([])
|
||||
const rounds = ref([])
|
||||
const winners = ref([])
|
||||
const isRolling = ref(false)
|
||||
const currentRound = ref(null)
|
||||
const displayMode = ref('scroll')
|
||||
const backgroundImage = ref('')
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// 从IndexedDB初始化数据
|
||||
const initFromIndexedDB = async () => {
|
||||
if (isInitialized.value) return
|
||||
|
||||
try {
|
||||
console.log('Display: Initializing from IndexedDB...')
|
||||
const data = await indexedDB.getAll()
|
||||
console.log('Display: Retrieved data keys:', Object.keys(data))
|
||||
|
||||
fields.value = data.lottery_fields || []
|
||||
participants.value = data.lottery_participants || []
|
||||
prizes.value = data.lottery_prizes || []
|
||||
rounds.value = data.lottery_rounds || []
|
||||
winners.value = data.lottery_winners || []
|
||||
isRolling.value = data.lottery_isRolling === 'true'
|
||||
currentRound.value = data.lottery_currentRound || null
|
||||
displayMode.value = data.lottery_displayMode || 'scroll'
|
||||
backgroundImage.value = data.lottery_backgroundImage || ''
|
||||
|
||||
console.log('Display: Initialized background image:', backgroundImage.value ? 'Yes' : 'No')
|
||||
console.log('Display: Initialization completed')
|
||||
|
||||
isInitialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Display: Failed to initialize from IndexedDB:', error)
|
||||
// 使用默认值
|
||||
fields.value = [{key: 'name', label: '姓名', required: true}]
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initFromIndexedDB()
|
||||
|
||||
// 监听localStorage变化来触发数据同步
|
||||
const handleStorageChange = (e) => {
|
||||
if (e.key === 'lottery_data_changed') {
|
||||
console.log('Display: Storage change detected, reloading data...')
|
||||
initFromIndexedDB()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
}
|
||||
|
||||
// 轮询同步IndexedDB数据(作为备用)
|
||||
let syncInterval = null
|
||||
const startSync = () => {
|
||||
if (syncInterval) return
|
||||
|
||||
console.log('Display: Starting sync interval...')
|
||||
|
||||
syncInterval = setInterval(async () => {
|
||||
try {
|
||||
const data = await indexedDB.getAll()
|
||||
|
||||
// 只更新变化的值,避免不必要的重渲染
|
||||
if (JSON.stringify(data.lottery_fields) !== JSON.stringify(fields.value)) {
|
||||
fields.value = data.lottery_fields || []
|
||||
}
|
||||
if (JSON.stringify(data.lottery_participants) !== JSON.stringify(participants.value)) {
|
||||
participants.value = data.lottery_participants || []
|
||||
}
|
||||
if (JSON.stringify(data.lottery_prizes) !== JSON.stringify(prizes.value)) {
|
||||
prizes.value = data.lottery_prizes || []
|
||||
}
|
||||
if (JSON.stringify(data.lottery_rounds) !== JSON.stringify(rounds.value)) {
|
||||
rounds.value = data.lottery_rounds || []
|
||||
}
|
||||
if (JSON.stringify(data.lottery_winners) !== JSON.stringify(winners.value)) {
|
||||
winners.value = data.lottery_winners || []
|
||||
}
|
||||
if (data.lottery_isRolling !== isRolling.value) {
|
||||
isRolling.value = data.lottery_isRolling === 'true'
|
||||
}
|
||||
if (JSON.stringify(data.lottery_currentRound) !== JSON.stringify(currentRound.value)) {
|
||||
currentRound.value = data.lottery_currentRound || null
|
||||
}
|
||||
if (data.lottery_displayMode !== displayMode.value) {
|
||||
displayMode.value = data.lottery_displayMode || 'scroll'
|
||||
}
|
||||
if (data.lottery_backgroundImage !== backgroundImage.value) {
|
||||
console.log('Display: Background image changed from', backgroundImage.value ? 'Yes' : 'No', 'to', data.lottery_backgroundImage ? 'Yes' : 'No')
|
||||
backgroundImage.value = data.lottery_backgroundImage || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Display: Failed to sync data from IndexedDB:', error)
|
||||
}
|
||||
}, 1000) // 每秒同步一次
|
||||
}
|
||||
|
||||
const stopSync = () => {
|
||||
if (syncInterval) {
|
||||
clearInterval(syncInterval)
|
||||
syncInterval = null
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动同步
|
||||
startSync()
|
||||
|
||||
// ============ 只读方法 ============
|
||||
const getPrizeForRound = (roundId) => {
|
||||
const round = rounds.value.find(r => r.id === roundId)
|
||||
if (!round) return null
|
||||
return prizes.value.find(p => p.id === round.prizeId)
|
||||
}
|
||||
|
||||
const switchDisplayMode = (mode) => {
|
||||
displayMode.value = mode
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
fields,
|
||||
participants,
|
||||
prizes,
|
||||
rounds,
|
||||
winners,
|
||||
isRolling,
|
||||
currentRound,
|
||||
displayMode,
|
||||
backgroundImage,
|
||||
isInitialized,
|
||||
|
||||
// 只读方法
|
||||
getPrizeForRound,
|
||||
switchDisplayMode,
|
||||
startSync,
|
||||
stopSync
|
||||
}
|
||||
})
|
||||
30
display/src/style.css
Normal file
30
display/src/style.css
Normal file
@@ -0,0 +1,30 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
93
display/src/styles/variables.css
Normal file
93
display/src/styles/variables.css
Normal file
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
/* 颜色系统 - 参考项目配色 */
|
||||
--color-primary: rgb(29, 17, 17);
|
||||
--color-primary-rgb: 29, 17, 17;
|
||||
|
||||
--color-secondary: rgb(0, 165, 141);
|
||||
--color-secondary-rgb: 0, 165, 141;
|
||||
|
||||
--color-accent: rgb(34, 37, 121);
|
||||
--color-accent-rgb: 34, 37, 121;
|
||||
|
||||
--color-success: rgb(0, 165, 141);
|
||||
--color-warning: rgb(255, 193, 7);
|
||||
--color-danger: rgb(220, 53, 69);
|
||||
--color-info: rgb(23, 162, 184);
|
||||
|
||||
--color-text-primary: rgb(51, 51, 51);
|
||||
--color-text-secondary: rgb(108, 117, 125);
|
||||
--color-text-light: rgb(173, 181, 189);
|
||||
--color-text-white: rgb(255, 255, 255);
|
||||
|
||||
--color-background: rgb(248, 249, 250);
|
||||
--color-border: rgb(222, 226, 230);
|
||||
--color-border-light: rgb(233, 236, 239);
|
||||
|
||||
/* 字体系统 */
|
||||
--font-family-primary: 'Poppins', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-secondary: 'Montserrat', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 24px;
|
||||
--font-size-3xl: 32px;
|
||||
--font-size-4xl: 48px;
|
||||
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* 间距系统 */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 12px;
|
||||
--spacing-lg: 16px;
|
||||
--spacing-xl: 20px;
|
||||
--spacing-2xl: 24px;
|
||||
--spacing-3xl: 32px;
|
||||
--spacing-4xl: 40px;
|
||||
|
||||
/* 圆角系统 */
|
||||
--border-radius-sm: 4px;
|
||||
--border-radius-md: 8px;
|
||||
--border-radius-lg: 12px;
|
||||
--border-radius-xl: 16px;
|
||||
--border-radius-full: 50%;
|
||||
|
||||
/* 阴影系统 */
|
||||
--shadow-small: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
--shadow-large: 0 8px 32px rgba(0, 0, 0, 0.16);
|
||||
--shadow-button: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* 过渡动画 */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
--transition-slow: 0.5s ease;
|
||||
--transition-color: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
--transition-background: background-color 0.3s ease;
|
||||
--transition-border: border-color 0.3s ease;
|
||||
--transition-box-shadow: box-shadow 0.3s ease;
|
||||
--transition-transform: transform 0.3s ease;
|
||||
|
||||
/* 行高 */
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
|
||||
/* 布局 */
|
||||
--header-height: 64px;
|
||||
--sidebar-height: 64px;
|
||||
--button-height-sm: 32px;
|
||||
--button-height-md: 40px;
|
||||
--button-height-lg: 48px;
|
||||
|
||||
/* 卡片 */
|
||||
--card-border-radius: 12px;
|
||||
--card-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
1
display/src/typescript.svg
Normal file
1
display/src/typescript.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
26
display/tsconfig.json
Normal file
26
display/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
13
display/vite.config.js
Normal file
13
display/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@utils': path.resolve(__dirname, '../utils')
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user