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

+152
View File
@@ -0,0 +1,152 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import MxIcon from '../components/MxIcon.vue'
import { mockMode, mockReason } from '../api/client'
import { useAuthStore } from '../stores/auth'
import { useEventStore } from '../stores/event'
import { useToastStore } from '../stores/toast'
const auth = useAuthStore()
const data = useEventStore()
const toast = useToastStore()
const router = useRouter()
const username = ref('')
const password = ref('')
const canSubmit = computed(() => username.value.trim() !== '' && password.value !== '' && !auth.loading)
async function submit(): Promise<void> {
if (!canSubmit.value) return
const ok = await auth.login(username.value.trim(), password.value)
if (!ok) {
toast.error(auth.error ?? '登录失败')
return
}
toast.success(`欢迎回来,${auth.displayName}`)
await data.load()
void router.push({ name: 'entry' })
}
function asGuest(): void {
toast.info('已进入只读访客视图')
void router.push({ name: 'standings' })
}
</script>
<template>
<div class="login-page">
<div class="mx-card login-card">
<div class="mx-card-body">
<div class="login-brand">
<span class="mx-logo-icon"><MxIcon name="standings" :size="20" /></span>
<div>
<h1>积分榜管理面板</h1>
<p>2026 职工运动会 · 成绩录入与实时积分</p>
</div>
</div>
<form class="login-form" @submit.prevent="submit">
<label class="mx-label" for="username">账号</label>
<div class="mx-input-wrap">
<span class="mx-input-icon"><MxIcon name="user" :size="16" /></span>
<input id="username" v-model="username" class="mx-input" autocomplete="username" placeholder="录入员 / 裁判长" />
</div>
<label class="mx-label" for="password">密码</label>
<div class="mx-input-wrap">
<span class="mx-input-icon"><MxIcon name="lock" :size="16" /></span>
<input
id="password"
v-model="password"
class="mx-input"
type="password"
autocomplete="current-password"
placeholder="请输入密码"
/>
</div>
<button class="mx-btn mx-btn-primary mx-btn-lg login-submit" type="submit" :disabled="!canSubmit">
<MxIcon name="check" :size="16" />
{{ auth.loading ? '登录中…' : '登录' }}
</button>
</form>
<div class="login-divider"><span>或</span></div>
<button class="mx-btn mx-btn-secondary login-submit" type="button" @click="asGuest">
以访客身份只读浏览
</button>
<p v-if="mockMode" class="mx-badge mx-badge-warning login-mock">
示例数据:{{ mockReason ?? '服务端未就绪' }},登录后操作不会写入真实赛事
</p>
</div>
</div>
</div>
</template>
<style scoped>
.login-page {
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background:
radial-gradient(1200px 600px at 80% -10%, var(--mx-primary-glow), transparent 60%),
var(--mx-bg);
}
.login-card {
width: 100%;
max-width: 420px;
}
.login-brand {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 24px;
}
.login-brand h1 {
font-size: 19px;
font-weight: 800;
}
.login-brand p {
font-size: 12px;
color: var(--mx-t3);
margin-top: 3px;
}
.login-form {
display: flex;
flex-direction: column;
}
.login-form .mx-label:not(:first-child) {
margin-top: 16px;
}
.login-submit {
width: 100%;
margin-top: 20px;
}
.login-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 18px 0;
color: var(--mx-t3);
font-size: 12px;
}
.login-divider::before,
.login-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--mx-divider);
}
.login-mock {
margin-top: 18px;
width: 100%;
justify-content: center;
text-align: center;
}
</style>