初始化提交

This commit is contained in:
yuantao
2026-01-15 10:38:00 +08:00
commit 96ccf6430a
58 changed files with 13856 additions and 0 deletions

24
display/.gitignore vendored Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

20
display/package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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);
}

View 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
View 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
View 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')
}
}
})