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

154 lines
4.1 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, 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: auth.user?.mustChangePassword ? 'password' : '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">
<img class="app-icon" src="/scoreboard.png" alt="" width="36" height="36" />
<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>