feat(server): 用户管理与变更历史接口;修复 result_revisions.parent_id 从未写入

This commit is contained in:
AskaEth committed 2026-09-09 19:51:50 +08:00
1 parent f3813171d3
commit 72462792ec
39 files changed
+10001 -28

No files matched your search

+41 -17
View File
@@ -5,9 +5,11 @@ import { mix, parseHexColor, readableForeground, toCss } from './color'
import type { DisplayPageKey } from './types'
import { useScoreboard } from './useScoreboard'
import { useStageScale } from './useStageScale'
import { WINDOWED_TITLEBAR_HEIGHT, isWindowed } from './windowed'
import DetailOverlay from './components/DetailOverlay.vue'
import TickerBar from './components/TickerBar.vue'
import TopBar from './components/TopBar.vue'
import WindowedTitleBar from './components/WindowedTitleBar.vue'
import AnnouncementPage from './components/pages/AnnouncementPage.vue'
import AthleteStandingsPage from './components/pages/AthleteStandingsPage.vue'
import EventResultsPage from './components/pages/EventResultsPage.vue'
@@ -23,10 +25,19 @@ const PAGES: Record<DisplayPageKey, Component> = {
}
const { bootstrap, dispose, page, settings } = useScoreboard()
const { scale, offsetX, offsetY } = useStageScale()
// 单屏窗口化才有标题栏;kiosk 下 windowed 为 false,下面所有相关分支都不生效
const windowed = isWindowed()
const { scale, offsetX, offsetY } = useStageScale({
topInset: () => (windowed ? WINDOWED_TITLEBAR_HEIGHT : 0),
})
const currentPage = computed<Component>(() => PAGES[page.value])
const rootStyle = { '--display-titlebar-h': `${WINDOWED_TITLEBAR_HEIGHT}px` }
const barTitle = computed(() => settings.value?.display.title.trim() || '赛事标题待配置')
const barLogo = computed(() => settings.value?.display.logo?.trim() ?? '')
const stageStyle = computed(() => ({
transform: `scale(${scale.value})`,
left: `${offsetX.value}px`,
@@ -72,30 +83,43 @@ onUnmounted(() => {
</script>
<template>
<div class="screen">
<div class="stage" :style="[stageStyle, themeVars]">
<img v-if="background" class="bg" :src="background" alt="" />
<div class="bg-fallback" />
<div class="root" :style="rootStyle">
<WindowedTitleBar v-if="windowed" :title="barTitle" :logo="barLogo" />
<div class="content">
<TopBar />
<main class="main">
<Transition name="page" mode="out-in">
<component :is="currentPage" :key="page" />
</Transition>
</main>
<TickerBar />
<div class="screen">
<div class="stage" :style="[stageStyle, themeVars]">
<img v-if="background" class="bg" :src="background" alt="" />
<div class="bg-fallback" />
<div class="content">
<TopBar />
<main class="main">
<Transition name="page" mode="out-in">
<component :is="currentPage" :key="page" />
</Transition>
</main>
<TickerBar />
</div>
<DetailOverlay />
</div>
<DetailOverlay />
</div>
</div>
</template>
<style scoped>
.root {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: #000;
}
/* kiosk 下 root 里只有 .screen,flex:1 等价于原来的铺满视口 */
.screen {
position: fixed;
inset: 0;
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
background: #000;
cursor: none;
@@ -0,0 +1,89 @@
<script setup lang="ts">
/**
* 窗口化模式标题栏(单屏调试用)。
*
* 样式对齐 Console 自绘标题栏(console/renderer/styles.css 的 .topbar):
* 同样的 50px 高、8×22 圆角标记、--panel 底色与 --border 分隔线、整条可拖拽。
* 这里不引用 MIUIx 令牌——大屏端不加载管理端样式(07 第 9 节),所以直接写死同一组色值。
* 不放窗口控制按钮:大屏窗口由 Console 统一管理,本条只负责拖拽与提示。
*/
defineProps<{
title: string
logo: string
}>()
</script>
<template>
<header class="titlebar">
<div class="brand">
<img v-if="logo" class="brand-logo" :src="logo" alt="会标" />
<span v-else class="brand-mark" aria-hidden="true" />
<span class="brand-title">{{ title }}</span>
</div>
<span class="hint">按 <kbd>F11</kbd> 切换全屏</span>
</header>
</template>
<style scoped>
.titlebar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
height: var(--display-titlebar-h, 50px);
flex: 0 0 auto;
padding: 0 16px;
border-bottom: 1px solid #27314a;
background: #161d2d;
color: #e6ecf7;
user-select: none;
/* 无边框窗口靠这条拖动;条内没有可点控件,整条都是拖拽区 */
-webkit-app-region: drag;
cursor: default;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.brand-mark {
width: 8px;
height: 22px;
flex: 0 0 auto;
border-radius: 4px;
background: linear-gradient(180deg, #4fd8ff, #1b6bb8);
}
.brand-logo {
height: 26px;
max-width: 96px;
flex: 0 0 auto;
object-fit: contain;
}
.brand-title {
font-size: 15px;
font-weight: 600;
line-height: 1.25;
letter-spacing: 0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hint {
flex: 0 0 auto;
font-size: 11px;
line-height: 1.3;
color: #8b98b4;
white-space: nowrap;
}
kbd {
padding: 1px 6px;
border: 1px solid #27314a;
border-radius: 5px;
background: #1d2637;
font-family: inherit;
font-size: 11px;
color: #e6ecf7;
}
</style>
+12 -2
View File
@@ -3,20 +3,30 @@ import { onUnmounted, ref } from 'vue'
export const DESIGN_WIDTH = 1920
export const DESIGN_HEIGHT = 1080
export interface StageScaleOptions {
/**
* 窗口顶部被标题栏占掉的高度(kiosk 为 0)。
* 传 getter 而不是数值:缩放回调在 resize 时重新取值,避免外部状态变化后拿到旧值。
*/
topInset?: () => number
}
/**
* 1920×1080 设计基准 + `transform: scale()` 等比适配(06 第 3 节)。
*
* 只按「较小的一边」缩放并居中,非 16:9 窗口留黑边而不拉伸;
* 不写死设备像素,1080p / 4K / 系统缩放都能正确铺满。
*/
export function useStageScale() {
export function useStageScale(options: StageScaleOptions = {}) {
const topInset = options.topInset ?? ((): number => 0)
const scale = ref(1)
const offsetX = ref(0)
const offsetY = ref(0)
function fit(): void {
const width = window.innerWidth
const height = window.innerHeight
// 可用高度要扣掉窗口化标题栏,否则 1920×1080 舞台会被标题栏遮住顶部
const height = Math.max(0, window.innerHeight - topInset())
const next = Math.min(width / DESIGN_WIDTH, height / DESIGN_HEIGHT)
scale.value = next
offsetX.value = (width - DESIGN_WIDTH * next) / 2
+14
View File
@@ -0,0 +1,14 @@
/**
* 窗口化模式标记。
*
* 单屏调试时 Console 会给 URL 追加 `windowed=1`(06 第 6.3 节),页面据此画标题栏;
* 多屏 kiosk 不追加,页面必须保持零占用的全屏布局。
*/
export const WINDOWED_PARAM = 'windowed'
/** 标题栏高度:与 Console 自绘标题栏一致,缩放计算要减掉这一段。 */
export const WINDOWED_TITLEBAR_HEIGHT = 50
export function isWindowed(): boolean {
return new URLSearchParams(window.location.search).get(WINDOWED_PARAM) === '1'
}