154 lines
4.1 KiB
Vue
154 lines
4.1 KiB
Vue
<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>
|