feat: M3-M7 全模块落地 + 集成接线

- M3 认证/权限/审计/限流 + 全部 REST 接口
- M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制)
- M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配)
- M6 WebSocket 实时同步中心
- M7 xlsx 导出 + 赛事包 zip + 备份
- 规则包 YAML 导入导出(差异预览)
- 字体 vendor 子集化(4 套 x2 字重)
- 集成:realtime/export/rules 接入 main+httpapi;backup 子命令
- 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
This commit is contained in:
AskaEth committed 2026-09-09 19:02:24 +08:00
1 parent d0e3b93e76
commit bc8b1ee6c0
145 files changed
+25159 -321

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
<script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue'
import { formatClock } from '@/shared'
import { useScoreboard } from '../useScoreboard'
import OfflineBadge from './OfflineBadge.vue'
const { settings, options } = useScoreboard()
const now = ref(new Date().toISOString())
const timer = window.setInterval(() => {
now.value = new Date().toISOString()
}, 15_000)
onUnmounted(() => window.clearInterval(timer))
const display = computed(() => settings.value?.display ?? null)
const title = computed(() => display.value?.title.trim() || '赛事标题待配置')
const subtitle = computed(() => display.value?.subtitle?.trim() ?? '')
const host = computed(() => settings.value?.event.host?.trim() || '主办方待配置')
const logo = computed(() => display.value?.logo?.trim() || '')
const clock = computed(() => formatClock(now.value))
const identity = computed(() => {
const parts: string[] = []
if (options.value.instance !== null) parts.push(`实例 ${options.value.instance}`)
if (options.value.screen !== null) parts.push(`屏 ${options.value.screen}`)
return parts.join(' · ')
})
</script>
<template>
<header class="top">
<div class="logo">
<img v-if="logo" :src="logo" alt="" />
<span v-else>会标</span>
</div>
<div class="titles">
<h1>{{ title }}</h1>
<p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
</div>
<div class="right">
<div class="clock">{{ clock }}</div>
<div class="host">{{ host }}</div>
<div v-if="identity" class="identity">{{ identity }}</div>
<OfflineBadge />
</div>
</header>
</template>
<style scoped>
.top {
display: flex;
align-items: center;
gap: 28px;
min-height: 132px;
flex: none;
}
.logo {
width: 108px;
height: 108px;
flex: none;
border-radius: 50%;
border: 3px solid var(--primary);
display: grid;
place-items: center;
font-size: 22px;
letter-spacing: 2px;
color: var(--text-dim);
overflow: hidden;
}
.logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.titles {
min-width: 0;
}
h1 {
font-size: 62px;
line-height: 1.1;
letter-spacing: 8px;
white-space: nowrap;
background: linear-gradient(180deg, #ffffff 25%, var(--primary-light) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.subtitle {
margin-top: 8px;
font-size: 26px;
letter-spacing: 6px;
color: var(--text-dim);
}
.right {
margin-left: auto;
text-align: right;
flex: none;
}
.clock {
font-size: 44px;
font-weight: 700;
letter-spacing: 3px;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.host {
margin-top: 6px;
font-size: 24px;
letter-spacing: 3px;
color: var(--text-dim);
}
.identity {
margin-top: 4px;
font-size: 18px;
letter-spacing: 1px;
color: rgba(157, 180, 212, 0.6);
}
</style>