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

+315
View File
@@ -0,0 +1,315 @@
<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)
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
}
if (!data.loaded) void data.load()
})
</script>
<template>
<div class="about-page">
<PageHeader title="关于" subtitle="系统信息、技术栈与部署参数" />
<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>