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:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user