Files
Scoreboard/web/src/admin/pages/AboutPage.vue
T

344 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import PageHeader from '../components/PageHeader.vue'
import { adminApi } from '../api/client'
import { useEventStore } from '../stores/event'
const data = useEventStore()
const SYSTEM_NAME = '运动会积分榜系统'
const SYSTEM_EN = 'Scoreboard'
// /api/health 只回 version,没有构建时间字段;构建期常量要改 vite.config,本页按约定显示 dev
const BUILD_TIME = 'dev'
const version = ref<string | null>(null)
const versionChecked = ref(false)
const versionText = computed(() => {
if (version.value !== null) return version.value
return versionChecked.value ? '未知(/api/health 不可达)' : '读取中…'
})
interface InfoItem {
name: string
usage: string
license: string
note?: string
}
const stack: InfoItem[] = [
{ name: 'Go 1.26', usage: '服务端、实时协议、静态资源内嵌', license: 'MIT(本项目)' },
{ name: 'Vue 3 + TypeScript', usage: '管理端与大屏前端', license: 'MIT' },
{ name: 'Vite', usage: '前端构建与开发服务', license: 'MIT' },
{ name: 'Pinia + Vue Router', usage: '管理端状态与路由', license: 'MIT' },
{ name: 'Electron', usage: 'Scoreboard Console 桌面宿主', license: 'MIT' },
{ name: 'MIUIx', usage: '管理端界面风格(vendor 内置,不走 CDN)', license: '随仓库分发的样式参考实现' },
]
const fonts: InfoItem[] = [
{ name: 'Noto Sans SC', usage: '默认界面字体', license: 'OFL-1.1' },
{ name: 'Noto Serif SC', usage: '标题衬线字体', license: 'OFL-1.1' },
{ name: 'Source Han Sans CN(思源黑体)', usage: '备用无衬线字体', license: 'OFL-1.1' },
{
name: 'MiSans',
usage: '可选界面字体',
license: 'MiSans Font License',
note: 'MiSans 仅限内部赛事使用,不得对外分发',
},
]
interface ShortcutItem {
keys: string[]
desc: string
}
const entryShortcuts: ShortcutItem[] = [
{ keys: ['Tab'], desc: '跳到下一个单元格(浏览器原生焦点顺序)' },
{ keys: ['Enter'], desc: '在成绩格之间移动' },
{ keys: ['Ctrl', 'Enter'], desc: '保存草稿' },
{ keys: ['Alt', 'Enter'], desc: '发布当前项目成绩' },
]
const displayShortcuts: ShortcutItem[] = [
{ keys: ['F11'], desc: '单屏窗口化模式下切换全屏;多屏 kiosk 不响应' },
]
const eventName = computed(() => data.settings?.event.name.trim() || '未配置')
const port = computed(() => {
if (window.location.port !== '') return window.location.port
return window.location.protocol === 'https:' ? '443' : '80'
})
const origin = computed(() => window.location.origin)
// 内部署名由构建期注入,只有登录后(recorder+)才拿得到;未注入时为空串
const internalSignature = ref('')
onMounted(async () => {
// 直接从 /api/health 取版本;不走 adminApi.health 的示例数据兜底,避免把 mock 当成真实版本
try {
const info = await adminApi.client.health()
version.value = info.version
} catch {
version.value = null
} finally {
versionChecked.value = true
}
try {
const res = await fetch('/api/system/about', { credentials: 'include' })
if (res.ok) {
const body = (await res.json()) as { ok: boolean; data?: { internalSignature?: string } }
internalSignature.value = body.data?.internalSignature ?? ''
}
} catch {
// 未登录 / 接口不可达都按「没有署名」处理,不打扰用户
}
if (!data.loaded) void data.load()
})
</script>
<template>
<div class="about-page">
<PageHeader title="关于" subtitle="系统信息、技术栈与部署参数" />
<div style="display: flex; align-items: center; gap: 14px; margin-bottom: 16px">
<img class="app-icon" src="/scoreboard.png" alt="" width="56" height="56" />
<div style="display: flex; flex-direction: column; gap: 2px">
<strong style="font-size: 18px">{{ SYSTEM_NAME }}</strong>
<span style="font-size: 12px; color: var(--mx-t3)">{{ SYSTEM_EN }} · v{{ versionText }}</span>
</div>
</div>
<div v-if="internalSignature" class="mx-card" style="margin-bottom: 16px">
<div class="mx-card-body" style="display: flex; flex-direction: column; gap: 4px">
<span style="font-size: 12px; color: var(--mx-t3)">内部署名</span>
<strong style="font-size: 15px">{{ internalSignature }}</strong>
</div>
</div>
<section class="mx-card">
<div class="mx-card-head">
<span class="mx-card-title">基本信息</span>
</div>
<div class="mx-card-body">
<dl class="kv">
<div>
<dt>系统名称</dt>
<dd>
{{ SYSTEM_NAME }}
<span class="en">{{ SYSTEM_EN }}</span>
</dd>
</div>
<div>
<dt>版本号</dt>
<dd class="mono">{{ versionText }}</dd>
</div>
<div>
<dt>构建时间</dt>
<dd class="mono">{{ BUILD_TIME }}</dd>
</div>
<div>
<dt>运行模式</dt>
<dd>{{ data.isMock ? '示例数据' : '真实服务端' }}</dd>
</div>
</dl>
</div>
</section>
<section class="mx-card">
<div class="mx-card-head">
<span class="mx-card-title">技术栈与开源许可</span>
<span class="mx-badge mx-badge-neutral">MIT · OFL-1.1</span>
</div>
<div class="mx-card-body no-pad">
<div class="mx-table-wrap">
<table class="mx-table mx-table-compact">
<thead>
<tr>
<th>组件</th>
<th>用途</th>
<th>许可</th>
</tr>
</thead>
<tbody>
<tr v-for="item in stack" :key="item.name">
<td class="name">{{ item.name }}</td>
<td class="muted">{{ item.usage }}</td>
<td>{{ item.license }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="mx-card">
<div class="mx-card-head">
<span class="mx-card-title">字体清单</span>
</div>
<div class="mx-card-body no-pad">
<div class="mx-table-wrap">
<table class="mx-table mx-table-compact">
<thead>
<tr>
<th>字体</th>
<th>用途</th>
<th>授权</th>
</tr>
</thead>
<tbody>
<tr v-for="font in fonts" :key="font.name">
<td class="name">{{ font.name }}</td>
<td class="muted">{{ font.usage }}</td>
<td>
{{ font.license }}
<span v-if="font.note" class="warn-note">{{ font.note }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="mx-card">
<div class="mx-card-head">
<span class="mx-card-title">键盘快捷键</span>
</div>
<div class="mx-card-body no-pad">
<div class="mx-table-wrap">
<table class="mx-table mx-table-compact">
<thead>
<tr>
<th>位置</th>
<th>按键</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr v-for="item in entryShortcuts" :key="`entry-${item.desc}`">
<td class="name">成绩录入</td>
<td class="keys">
<template v-for="(key, index) in item.keys" :key="key">
<span v-if="index > 0" class="plus">+</span><kbd>{{ key }}</kbd>
</template>
</td>
<td class="muted">{{ item.desc }}</td>
</tr>
<tr v-for="item in displayShortcuts" :key="`display-${item.desc}`">
<td class="name">大屏</td>
<td class="keys">
<template v-for="(key, index) in item.keys" :key="key">
<span v-if="index > 0" class="plus">+</span><kbd>{{ key }}</kbd>
</template>
</td>
<td class="muted">{{ item.desc }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="mx-card">
<div class="mx-card-head">
<span class="mx-card-title">部署信息</span>
</div>
<div class="mx-card-body">
<dl class="kv">
<div>
<dt>数据目录</dt>
<dd class="mono">— <span class="muted">(服务端未暴露该接口)</span></dd>
</div>
<div>
<dt>服务端口</dt>
<dd class="mono">{{ port }}</dd>
</div>
<div>
<dt>访问地址</dt>
<dd class="mono">{{ origin }}</dd>
</div>
<div>
<dt>赛事项目名</dt>
<dd>{{ eventName }}</dd>
</div>
</dl>
</div>
</section>
<p class="foot-hint">
本系统为内部赛事使用;MiSans 字体授权仅限内部赛事,对外分发前请替换为 OFL-1.1 字体。
</p>
</div>
</template>
<style scoped>
.about-page {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 960px;
}
.kv {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 14px 24px;
}
.kv > div {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.kv dt {
font-size: 12px;
color: var(--mx-t3);
}
.kv dd {
font-size: 14px;
word-break: break-all;
}
.en {
margin-left: 6px;
font-size: 12px;
color: var(--mx-t3);
}
.no-pad {
padding: 0;
}
.name {
white-space: nowrap;
}
.muted {
color: var(--mx-t3);
}
.warn-note {
display: block;
margin-top: 2px;
font-size: 11px;
color: var(--mx-amber, #fbbf24);
}
.keys {
white-space: nowrap;
}
.plus {
margin: 0 3px;
color: var(--mx-t3);
}
kbd {
padding: 1px 6px;
border: 1px solid var(--mx-border);
border-radius: 5px;
background: var(--mx-s3);
font-family: var(--mx-mono);
font-size: 11px;
}
.foot-hint {
font-size: 12px;
color: var(--mx-t3);
}
</style>